A review scenario keeps repeating in tickets. A team points a new service at free compute and calls it a spike. Nobody writes down the traits that would have disqualified the workload. Six weeks later the service is a dependency and the exit conversation starts late.

That story is illustrative, not a specific company. The failure mode is generic. Reviewers remember to ask what a workload needs. They forget to ask what a free shared box cannot honestly provide.

This article replaces the checklist with a gate. The gate runs in CI, returns an exit code, and refuses deployments. It ships with a test suite so the policy itself is tested.

Why a checklist loses to a gate

A checklist depends on a human reading it at the right moment. Gates do not. A gate fails the build before the deploy exists.

Three properties matter more than the rule list.

  1. The verdict is machine-readable: allow, allow_with_exit, or refuse.
  2. Reasons are attached to each verdict, so the author learns the constraint.
  3. The policy file is versioned, so threshold changes go through review.

A reviewer can ignore a wiki page. A reviewer cannot ignore exit code 2.

The artifact: gate.py

Install two dependencies. Nothing else is required.

pip install pyyaml pytest 
# gate.py from __future__ import annotations import json import sys from dataclasses import dataclass from enum import Enum from pathlib import Path import yaml class Verdict(str, Enum): ALLOW = "allow" ALLOW_WITH_EXIT = "allow_with_exit" REFUSE = "refuse" EXIT_CODES = {Verdict.ALLOW: 0, Verdict.ALLOW_WITH_EXIT: 1, Verdict.REFUSE: 2} @dataclass(frozen=True) class Workload: name: str p95_budget_ms: int peak_rps: int stores_pii: bool needs_local_disk: bool max_job_seconds: int revenue_path: bool team_oncall: bool tokens_per_request: int def evaluate(w: Workload, policy: dict) -> list[tuple[Verdict, str]]: hits: list[tuple[Verdict, str]] = [] if w.stores_pii: hits.append((Verdict.REFUSE, "stores_pii: a shared free host is not an auditable data boundary")) if w.needs_local_disk: hits.append((Verdict.REFUSE, "needs_local_disk: sticky state is destroyed with the host")) if w.max_job_seconds > policy["max_job_seconds"]: hits.append((Verdict.REFUSE, f"job runtime {w.max_job_seconds}s exceeds the preemption window")) if w.peak_rps > policy["peak_rps"]: hits.append((Verdict.REFUSE, f"peak {w.peak_rps} rps exceeds your own ceiling of {policy['peak_rps']}")) if w.revenue_path and not w.team_oncall: hits.append((Verdict.REFUSE, "revenue path without an on-call owner")) if w.p95_budget_ms < policy["p95_budget_ms"]: hits.append((Verdict.ALLOW_WITH_EXIT, "tight p95 budget: define a measured exit line first")) if w.tokens_per_request > policy["tokens_per_request"]: hits.append((Verdict.ALLOW_WITH_EXIT, "high per-request token burn: set a daily cap and alert")) if not w.team_oncall: hits.append((Verdict.ALLOW_WITH_EXIT, "no on-call: schedule a revoke rehearsal")) return hits def decide(hits: list[tuple[Verdict, str]]) -> Verdict: if any(v is Verdict.REFUSE for v, _ in hits): return Verdict.REFUSE if hits: return Verdict.ALLOW_WITH_EXIT return Verdict.ALLOW def main(argv: list[str]) -> int: doc = yaml.safe_load(Path(argv[1]).read_text()) workload = Workload(**doc["workload"]) hits = evaluate(workload, doc["policy"]) verdict = decide(hits) print(json.dumps({ "workload": workload.name, "verdict": verdict.value, "reasons": [reason for _, reason in hits], }, indent=2)) return EXIT_CODES[verdict] if __name__ == "__main__": sys.exit(main(sys.argv)) 

A profile file

One YAML file per candidate workload. The policy block stays shared across the repo.

# profiles/checkout-api.yaml policy: max_job_seconds: 900 peak_rps: 20 p95_budget_ms: 300 tokens_per_request: 20000 workload: name: checkout-api p95_budget_ms: 250 peak_rps: 120 stores_pii: true needs_local_disk: false max_job_seconds: 30 revenue_path: true team_oncall: true tokens_per_request: 1200 

Run it. The verdict is refuse for three independent reasons.

$ python -m gate profiles/checkout-api.yaml; echo "exit=$?" { "workload": "checkout-api", "verdict": "refuse", "reasons": [ "stores_pii: a shared free host is not an auditable data boundary", "peak 120 rps exceeds your own ceiling of 20", "revenue path without an on-call owner" ] } exit=2 

Test the policy, not just the code

The rules are the product. Parametrized tests keep the rule table honest.

# test_gate.py import pytest from gate import Workload, Verdict, decide, evaluate POLICY = {"max_job_seconds": 900, "peak_rps": 20, "p95_budget_ms": 300, "tokens_per_request": 20000} def make(**kw) -> Workload: base = dict(name="t", p95_budget_ms=800, peak_rps=4, stores_pii=False, needs_local_disk=False, max_job_seconds=60, revenue_path=False, team_oncall=True, tokens_per_request=2000) base.update(kw) return Workload(**base) @pytest.mark.parametrize("kw,expected", [ ({}, Verdict.ALLOW), ({"p95_budget_ms": 200}, Verdict.ALLOW_WITH_EXIT), ({"tokens_per_request": 60000}, Verdict.ALLOW_WITH_EXIT), ({"team_oncall": False}, Verdict.ALLOW_WITH_EXIT), ({"stores_pii": True}, Verdict.REFUSE), ({"needs_local_disk": True}, Verdict.REFUSE), ({"max_job_seconds": 3600}, Verdict.REFUSE), ({"peak_rps": 60}, Verdict.REFUSE), ({"revenue_path": True, "team_oncall": False}, Verdict.REFUSE), ({"revenue_path": True}, Verdict.ALLOW), ]) def test_verdicts(kw, expected): assert decide(evaluate(make(**kw), POLICY)) is expected 
$ pytest -q test_gate.py 10 passed in 0.04s 

Wire the gate into CI

Treat allow_with_exit as a warning and refuse as a hard stop. The set +e trick lets you branch on the code.

# .github/workflows/free-lane-gate.yml - name: Refusal gate run: | set +e python -m gate profiles/candidates/${{ inputs.profile }}.yaml code=$? if [ "$code" -eq 2 ]; then echo "::error::workload refused"; exit 2; fi if [ "$code" -eq 1 ]; then echo "::warning::allowed with exit criteria"; fi 

Now the policy lives next to the deploy. Nobody discovers the constraint after the incident review.

The eight traits, in plain language

Five traits refuse the workload outright.

  1. Personal data in scope. You cannot audit a shared host boundary you do not control.
  2. Sticky local disk. State on an ephemeral box is a countdown, not a design.
  3. Long-running jobs. Anything past your preemption window needs checkpointing you do not have.
  4. Sustained load above your ceiling. Spikes tolerate shared capacity. Baselines do not.
  5. Revenue path without an owner. If money moves, someone must hold a pager.

Three traits allow the workload only with a written exit.

  1. Tight p95 budget. Record the measured exit number before the first deploy.
  2. High token burn per request. Set a daily cap and alert on the burn rate, not the total.
  3. No on-call rotation. Schedule the revoke rehearsal as a calendar event.

The other half: revoke triggers

The gate decides entry. A small metrics review decides exit. Keep the triggers numeric.

Trigger Threshold Action
Daily token burn above the cap two days running freeze prompts, trim context
p95 drift 40% above the recorded baseline open the migration ticket
Cold starts more than ten in a day move the workload off shared capacity
Manual restarts two in a week treat as a stability failure
Error budget more than half consumed before mid-cycle start the fallback plan

Thresholds are your policy, not vendor guarantees. Write them down anyway. A number you chose beats a feeling you had.

Where a low-stakes free lane does fit

Some workloads pass the gate cleanly. Documentation bots over public docs pass. Batch eval harnesses over synthetic fixtures pass. Prototypes with a named owner and a two-week horizon pass.

MonkeyCode is one option for that narrow band. Its maintainers describe an open-source coding agent with free model access, including an introductory 10M-token allowance, plus a free server option. Treat those terms as operator-stated and re-read the current page before you commit a roadmap to them.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The honest use is to point the gate at four candidate workloads, keep the one that returns allow, and run it where abandonment costs nothing. You get a real deployment target and a real exit drill on the same afternoon. The free lanes are documented at MonkeyCode's site if you want that target.

Limitations you should accept up front

  • The thresholds are placeholders. Calibrate them against your own incident history, not this article.
  • The gate is a preflight, not a load test. It cannot prove capacity.
  • It cannot see secrets pasted into prompts. That needs a separate review.
  • Free-tier terms change without notice. Re-run the gate when they do.
  • A refuse verdict is a conversation starter, not a legal opinion.

Who should not use this approach

Skip the free lane entirely if any of these describe you.

  • Regulated data with an audit obligation.
  • A contractual p95 below 300ms.
  • Stateful services that assume a durable disk.
  • Jobs that routinely run longer than fifteen minutes.
  • Teams with no on-call rotation and no plan to start one.

Those workloads need owned infrastructure and an owner with a pager. The gate exists to say so early, in CI, before anyone builds a dependency on a lane that was never meant to hold it.