Anatomy of a 400 from Production
The chat assistant shipped on Tuesday and returned its first 400 context_length_exceeded on Wednesday, two messages after a user pasted a vendor contract. The deployed model's window is 8,192 tokens; the request carried 9,410. Nothing in the code counts anything: the service concatenates four streams and hopes.
Those four streams are the whole budget:
|<-------------------- context window: 8,192 tokens -------------------->|
[ system prompt ][ conversation history ][ retrieved chunks ][ reserved output ][margin]
612 variable variable 1,024 128
\____________________ what you send ______________________/
- System prompt — instructions, persona, tool and schema definitions. Fixed per request, paid on every call.
- Conversation history — every prior turn you resend. Grows until you evict something.
- Retrieved chunks — passages from search. Grows with
kand chunk size. - Reserved output — the tokens the model will generate. Predicted input plus
max_tokensmust fit inside the window or the provider rejects the request.
Nothing here amortizes: system prompt and history are re-billed in full on every request, so prompt length is a recurring cost decision rather than a one-time edit.
The arithmetic is subtraction, and it surprises people exactly once:
usable prompt = window - reserved output - safety margin
8,192 - 1,024 - 128 = 7,040 tokens for prompt + history + retrieval
Teams that skip the reserve meet the second failure mode immediately: a request that fills the window completely leaves no room for the answer. The model errors at generation start, or the answer truncates mid-sentence with finish_reason: length while still reading like a finished reply.
Two rules keep the arithmetic honest. The predicted input — system prompt, history, and chunks — plus the max_tokens reserve is checked against the window before dispatch. And the counts come from the model's tokenizer rather than characters, because How Tokenizers Work showed that ratio shifting with language and content.
Assembling Context for the Model decides what goes in; this topic decides how much fits and what leaves when it does not.
The Budget Arithmetic
Turn the subtraction into a function that also decides what gets evicted. Token counts arrive as integers from a local counter, so the entire budget runs on arithmetic:
def build_request(window, system, turns, chunks, max_output=1024, margin=128):
budget = window - max_output - margin # tokens the prompt may occupy
left = budget - system
kept_chunks = 0
for size in reversed(chunks): # evict the lowest-ranked chunk first
if size <= left:
kept_chunks += 1
left -= size
kept_turns = 0
for size in reversed(turns): # evict the oldest turn first
if size <= left:
kept_turns += 1
left -= size
return {'input': budget - left, 'chunks': kept_chunks,
'turns': kept_turns, 'free': left}
Input:
print(build_request(
window=8192,
system=612,
turns=[1240, 1180, 1305, 1150, 1260], # oldest -> newest
chunks=[318, 302, 295, 341, 287, 310], # top rank -> lowest rank
))
Output:
{'input': 6180, 'chunks': 6, 'turns': 3, 'free': 860}
Walk it once. The budget is 8,192 - 1,024 - 128 = 7,040; minus the 612-token system prompt leaves 6,428. All six chunks fit (1,853 tokens), leaving 4,575. History arrives oldest first but is consumed newest first: 1,260, then 1,150, then 1,305 fit (3,715 total), and the 1,180-token turn no longer fits — so the two oldest turns, 1,240 and 1,180, are dropped. The request sends 6,180 input tokens and reserves 1,024 for output: 7,204 against a window of 8,192, leaving 988 tokens of slack.
Two lines carry the policy. reversed(...) in both loops states what gets sacrificed first: chunks lose from the tail of the ranked list, history loses its oldest turns, so the freshest dialogue and the best evidence survive. And budget - left is computed after eviction — the number you send is what survived, not what you hoped to send.
The reserve is the line teams delete under deadline pressure. Remove max_output from the subtraction and requests pass validation, then fail at generation, or the answer stops at an arbitrary point. Headroom is not slack; it is the part of the window that makes an answer possible.
Eviction Order and Enforcement
Eviction is a policy, and the policy decides which failures users see. Choose it explicitly:
| Component | Evict from | Keep | Silent risk |
|---|---|---|---|
| History | Oldest turns first | Recent dialogue | Dropping the turn that carried the original request |
| Retrieval | Lowest-ranked chunks | Top-ranked evidence | The answer loses the passage holding the key number |
| System prompt | Never — shrink it by design | Standing rules | Trimming rules to save space removes guardrails |
Enforcement happens at three checkpoints:
- Estimate while assembling. Keep a running token count as you append each component — system, then turns, then chunks — and stop when the budget is spent. Waiting until assembly finishes lets one oversized turn poison the request.
- Validate before dispatch.
input + max_tokens > windowis a deterministic check; raise locally with both numbers instead of paying for a 400 the provider will return anyway. - Log the survivors. Record input tokens, kept turns, kept chunks, and evictions per request. A steady climb in evictions is the early warning that history or retrieval is growing past budget — Conversation History covers trimming strategies and Token Usage Metering covers the logging layer.
The safety margin exists because your local counter and the provider's may differ slightly — tokenizer version drift, preprocessing of pasted text — and a 128-token cushion keeps that variance from becoming a 400. Treat the margin as an error bar, not as space to reclaim.
Failure modes worth naming:
- Characters instead of tokens — the estimate passes locally and the provider disagrees; the ratios live in How Tokenizers Work.
- Window filled to 100% — no reserve, so generation errors or the answer truncates mid-sentence.
- Evicting newest-first — drops the user's latest question, and the model confidently answers the previous turn.
- Retrying the oversized request unchanged — a wasted round trip that repeats the identical 400.
- Forgetting non-text parts — image tokens, tool schemas, and few-shot examples consume the same budget as prose.
A budget that survives production gives every component a ceiling, subtracts the reserve first, and logs evictions rather than discovering them through a confused user. Once the request fits, the surviving token counts feed Estimating Token Costs directly.
Quiz
1. A request passes your prompt-size check, but the provider returns context_length_exceeded. Which oversight explains it?
2. Why must output tokens be reserved before the prompt budget is filled?
3. Window 8,192, max_output 1,024, margin 128, system prompt 612. How many tokens may history and retrieval consume together?
4. When history must shrink to fit, which eviction order is correct and why?
Flashcards
Question
What four components consume the context window?
Click to reveal answer
Answer
System prompt, conversation history, retrieved chunks, and reserved output. The first three plus max_tokens must fit inside the window or the provider rejects the request.
Question
How do you compute the prompt budget?
Click to reveal answer
Answer
window - max_output - safety margin. Subtract the reserve first, then the fixed system prompt; whatever remains is the ceiling for history and retrieval combined.
Question
What happens if the prompt fills the window completely?
Click to reveal answer
Answer
There is no room to generate: the request errors at completion time, or the answer truncates mid-sentence with finish_reason 'length' while still reading like a finished reply.
Question
What is the correct eviction order when a request would overflow?
Click to reveal answer
Answer
Lowest-ranked retrieval chunks first, then oldest history turns. Evidence ranked last goes before dialogue ranked last; the system prompt is never trimmed on the fly.
Question
When should an oversized request be rejected?
Click to reveal answer
Answer
During assembly, before dispatch. input + max_tokens > window is a deterministic check, so fail locally with both numbers instead of paying for a provider round trip that returns 400.
Revision Notes
Key Takeaways
- 1. The window serves four consumers: system prompt, history, retrieval, and the output being generated
- 2. Subtract the output reserve and margin first; everything else fits inside what remains
- 3. Evict lowest-ranked chunks, then oldest turns — and log evictions instead of discovering them
- 4. Validate sizes locally before dispatch; an oversized request is a deterministic error, not a transient one
Interview Tips
- • Do the arithmetic out loud on a small window — 8,192 minus reserve minus margin — interviewers watch the order you subtract in
- • State an explicit eviction policy and defend it: why chunks lose before history, why oldest turns go first
- • Name both failure modes: overflowing the window (request rejected) and filling it exactly (answer truncated)
- • Connect the surviving token counts to cost: every kept chunk is billed on every request, which is where [Estimating Token Costs](/topic/aie-token-cost-estimation) begins
Cheat Sheet
Context Window Budgeting Cheat Sheet
Formula:
prompt_budget = window - max_output - margin
left = prompt_budget - system_tokens
# consume chunks worst-first, then turns oldest-first, tracking 'left'
input_sent = prompt_budget - left
fit check : input_sent + max_output <= window
Worked example (8,192 window):
| Step | Value |
|---|---|
| budget = 8192 - 1024 - 128 | 7,040 |
| minus system 612 | 6,428 |
| 6 chunks kept (1,853) | 4,575 |
| 3 newest turns kept (3,715) | 860 free |
| input sent | 6,180 (+ 1,024 reserved = 7,204) |
Eviction policy: chunks worst-ranked first, history oldest first, system prompt never.
Guardrails:
- Count with the model's tokenizer, never characters (How Tokenizers Work)
- Reserve output before filling the prompt; headroom makes the answer possible
- Validate before dispatch: prompt + max_tokens > window fails locally
- Log input tokens, kept turns, kept chunks, evictions per request
Failure modes: missing reserve, chars/4 estimates, newest-first eviction, blind retry of an oversized request, ignoring image/tool-schema tokens.
Continue Learning
Before You Start
- No prerequisites listed.