Your pager fires at 02:14 UTC during batch. Token spend sits near zero on the cost panel. Job p99 sits at forty-seven seconds against fifteen.
Free failover queue age reads thirty-eight seconds already. Remaining deadline slack is only nine seconds now. Which operational action follows from that evidence?
You do not enlarge the free pool tonight. You reject paid-to-free failover for that class.
Zero spend is not a recovery signal
A green cost panel can hide a dead SLO. Zero token spend often means jobs waited on free capacity. You paid with wall-clock time, not with tokens.
Deadline slack died while finance still looked healthy. Keep spend and latency on two separate ledgers. Never let a cheap dashboard veto a red latency page.
Topology for the local failover drill
Treat this as a labeled lab, not folklore. You need one admission process and two backends.
client -> admission (slack gate)
|-- paid_backend (reserved, metered)
|-- free_backend (best-effort FIFO)
|-- reject_429 (fail closed)
Keep both backends on separate local ports. Do not hide them behind one reverse proxy.
You must record which path each job took. Log backend, queue_age_ms, first_byte_ms, and remaining_slack_ms together.
Declared workload
Write the conditions down before any fault injection.
- Job class: 200-word summary, one shot, no tools
- Arrival: eight jobs per second for ninety seconds
- Deadline: fifteen seconds from enqueue timestamp
- Paid concurrency: two workers, 1200ms p95 first-byte
- Free concurrency: one worker after the FIFO wait
- Free first-byte p95 after queue: 4000ms
- Free queue: FIFO, no preemption, no reservation
- Retry: none on timeout; reject the job instead
- Success: first byte before slack falls under 2000ms
These figures are lab defaults only, not production. They are not product benchmarks or fleet observations.
Change them if your serving path differs. Keep every number declared in the runbook header.
When free capacity is the wrong bet
Paid-to-free failover looks smart during a burst. Overflow onto a free queue looks fine on invoices. All three fail when wait age exceeds remaining slack.
You are not buying inference in that window. You are buying a lottery ticket on queue delay.
Timeouts make the free-capacity bet worse fast. A timed-out job retries into the same cold FIFO.
Token spend stays near zero on every retry. Deadline miss rate climbs with each requeue.
That is still a cost incident for the SLO. The invoice is not the only bill you pay.
Artifact: slack-gated paid-to-free failover
Label this helper as a proposed local gate. It is a sketch, not a vendor client.
# slack_gate.py — proposed local admission helper
import json
import time
from dataclasses import dataclass
DEADLINE_MS = 15_000
RESERVE_MS = 2_000
PAID_P95_FIRST_BYTE_MS = 1_200
FREE_P95_FIRST_BYTE_MS = 4_000
FREE_QUEUE_AGE_CUT_MS = 3_000
@dataclass
class Snapshot:
paid_inflight: int
paid_limit: int
free_queue_age_ms: int
free_inflight: int
enqueued_at_ms: int
def remaining_slack_ms(now_ms: int, snap: Snapshot) -> int:
waited = now_ms - snap.enqueued_at_ms
return DEADLINE_MS - waited - RESERVE_MS
def admit(now_ms: int, snap: Snapshot) -> str:
slack = remaining_slack_ms(now_ms, snap)
if slack <= 0:
return "reject_deadline"
paid_ok = snap.paid_inflight < snap.paid_limit
if paid_ok and slack > PAID_P95_FIRST_BYTE_MS:
return "paid"
free_need = snap.free_queue_age_ms + FREE_P95_FIRST_BYTE_MS
age_ok = snap.free_queue_age_ms < FREE_QUEUE_AGE_CUT_MS
if free_need < slack and age_ok:
return "free"
return "reject_no_slack"
if __name__ == "__main__":
now = int(time.time() * 1000)
snap = Snapshot(
paid_inflight=2,
paid_limit=2,
free_queue_age_ms=8_000,
free_inflight=1,
enqueued_at_ms=now - 4_000,
)
decision = admit(now, snap)
print(json.dumps({
"decision": decision,
"slack_ms": remaining_slack_ms(now, snap),
"free_need_ms": snap.free_queue_age_ms + FREE_P95_FIRST_BYTE_MS,
}))
Run the helper once with that snapshot. Expected output for this labeled snapshot follows.
{"decision": "reject_no_slack", "slack_ms": 9000, "free_need_ms": 12000}
Paid workers sit full in this snapshot. Free path needs twelve seconds of wait plus infer.
You only hold nine seconds of remaining slack. You reject the failover instead of waiting cheaper.
Runbook table for the on-call
| paid inflight | free queue age | remaining slack | action |
|---|---|---|---|
| below limit | any | greater than paid p95 plus reserve | send paid |
| at limit | under 3s | greater than free p95 plus age | send free |
| at limit | 3s or more | any | reject 429 |
| any | any | at or below zero | reject 429 |
Pin the three-second free queue age cut. Rationale: age already spent the cheap part of slack.
Free worker utilization can still read twenty percent. One blocked first-byte hides behind that low CPU.
Queue age and slack beat utilization here. Do not add hosts because CPU looks idle.
Failure injection on the free backend
Do not wait for production to teach failover cost. Inject three faults against the local free stub.
- Sleep six seconds before the first response byte.
- Hold one extra FIFO job so age passes three seconds.
- Return HTTP 200 with an empty body after eight seconds.
Local stub
Proposed stub for fault one:
python3 - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
import time
class H(BaseHTTPRequestHandler):
def do_POST(self):
time.sleep(6)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"ok":true,"bytes":0}')
def log_message(self, *args):
pass
HTTPServer(("127.0.0.1", 8088), H).serve_forever()
PY
Drive the gate after the stub is up:
python3 slack_gate.py
# keep arrival at eight jobs per second for ninety seconds
What you should see
Watch these fields instead of the token counter:
-
decisionas paid, free, or reject_* -
queue_age_mson the free path only -
first_byte_msby path -
remaining_slack_msat admit time -
deadline_missafter the handler returns -
token_spendas a secondary ledger only
Expected observation under faults one and two follows. Reject count rises while deadline misses stay flat.
Token spend stays near zero on both paths. The useful page is reject_no_slack, not spend.
If misses rise with rejects, raise RESERVE_MS. Do not reopen unbounded paid-to-free failover tonight.
Telemetry fields to emit
Ship counters and histograms without blending them together.
job_decision{path="paid|free|reject"}
job_first_byte_ms{path="paid|free"}
job_queue_age_ms{path="free"}
job_remaining_slack_ms{stage="admit"}
job_deadline_miss{path="paid|free|reject"}
job_token_spend{path="paid|free"}
Alert on this contradiction and nothing else. Token spend falls while deadline misses rise together.
That pattern is zero-spend failover eating slack. Page on-call with queue age and slack samples. Leave the cost panel off the paging policy.
Use free access for the drill, not the SLO
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option. Use that pair to stage this failover drill.
Burn retries and reject paths there without an invoice. Keep the same slack gate on every request.
If free queue age plus first-byte exceeds slack, reject. Do not promote the free path onto SLO traffic.
Free capacity is a lab and an overflow probe. It is the wrong bet for hard deadline work.
Rollback and cleanup
Keep the gate behind an environment flag.
export ADMIT_FREE=0
# or force reject by cutting age to zero
export FREE_QUEUE_AGE_CUT_MS=0
systemctl reload admission
Drain free inflight before you touch paid workers. Do not SIGKILL paid workers during the drain.
curl -s localhost:9090/metrics | grep job_inflight
kill $(lsof -t -i:8088)
Revert lab p95 constants to paid measurements only. Delete the delay stub from staging DNS and unit files.
Record reject ratio in the incident note. Record that spend was not the recovery signal.
Who should not use this gate
Skip this gate for chat with no deadline. Skip it if you cannot measure first-byte wait. Skip it when a late answer beats a hard reject.
Batch research jobs can wait on free capacity. User-facing summaries with a fifteen-second SLO cannot.
If your only backend is free, you cannot fail over. Buy a reserved paid path before you add failover. Then place the slack gate in front of it.
Limits of this note
The p95 constants are declared lab values. They are not observations from a named fleet.
First-byte wait is not full generation time. A long completion can still miss after a fast first byte. Add a completion budget if the SLO covers the full answer.
This note ignores auth, prompt size, and cache hits. Those belong in a later operational drill instead. Do not copy the three-second cut without your own age histogram.
Threshold question for the next review
Which signal trips reject in your runbook today? Is it queue age, utilization, or remaining slack?
Utilization lies when one worker blocks on first byte. Queue age tells you wait already spent. Remaining slack tells you wait you can still afford.
Trip on slack versus queue age plus p95 first-byte. Write that comparison in the runbook before the next burst.
Top comments (0)