Skip to content

openai.BadRequestError: Error code: 400 - {'error': {'message': "This model's maximum context length is 16385 tokens. However, your messages resulted in 16648 tokens. Please reduce the length of the messages.", 'type': 'invalid_request_error', 'param': 'messages', 'code': 'context_length_exceeded'}}

This is the rare API failure that shows its working. A 400 means the request was understood and rejected on its contents, so your credential is fine (that would be 401), your budget is fine (429), and the platform is fine (503). The server counted your input, compared it to a ceiling, and told you both numbers. Everything that follows is arithmetic you can do yourself before sending — which is why the useful question is not “what went wrong” but “what should my handler do when it catches this”.

Data as of 2026-09. Vendor limits and defaults change; check the official docs for current values before acting on any number below.

The only fields worth branching on

The exception is a status error, so it carries status_code and the raw response. On the error body, two fields are stable enough to build logic on:

  • code is context_length_exceeded. This is the discriminator. It separates this from every other 400 — a bad parameter, a tool schema violation, an unsupported option — all of which share the invalid_request_error type and would otherwise be indistinguishable in a generic handler.
  • param is messages. It names the field that overflowed, which matters once your payload has several large parts.

The two numbers are only in message, as English prose. That is the trap, and it is the one experienced people fall into: writing a regular expression against message to extract the ceiling and the total, then using those to decide how much to trim. The code and param are contract; the sentence around them is copy. It can be reworded at any time, it is not the same sentence on every endpoint, and a regex that fails silently returns zero — after which your trimmer either does nothing or deletes the whole conversation.

Branch on code. Get the numbers from your own token count, not from the message. You need a local count anyway, because the only way to stop hitting this is to know the size before you send.

Is retrying useful?

No. The count is computed from a payload you control, and an unchanged payload produces an identical count every time.

The SDKs agree with that by omission. Automatic retry covers connection errors, 408, 409, 429 and 5xx — 400 is deliberately absent, so nothing was retried before you saw this. Unlike a throttling or transport failure, the exception in your hands is the first and only attempt, and no setting of max_retries changes that.

There is one legitimate-looking loop and it deserves a warning rather than a recommendation. “Catch, drop the oldest message, resend” is not a retry — it is a new request — and it is a reasonable pattern, but bound it by tokens, not by attempts. A single message that is larger than the window on its own makes a drop-oldest loop remove everything around it and still fail, and if your bound is an attempt count you will burn every one of them before finding that out. Compute the target size first, trim to it once, and send once.

The number that tells you which problem you have

There are two populations here and they need opposite fixes. Sending twice tells them apart, as long as you read the total rather than just the failure.

The total stays the same on every attempt — a single oversized payload. One document, one file, one pasted log. The request has never fit and never will, and nothing about your session history is involved. This is a reshaping problem.

The total grows on every attempt — an accumulating conversation. Each turn appends to messages, and you crossed the line on a specific turn. The important sub-case is self-inflicted: if your error handler appends the failed exchange, or re-appends the user’s message before retrying, every attempt is larger than the last. A loop like that diverges — it moves away from success while looking like it is working. Log the input token count on every attempt; two attempts with increasing totals identify this in seconds and nothing else does.

The total is far larger than you expected — something is in messages that you did not put there. Tool or function definitions, a retrieved-context block from a framework, a system prompt assembled from templates, or an entire prior tool result echoed back. Print the per-message token counts rather than the total; the outlier is usually obvious and usually not the part you were editing.

The window is not your input budget

Two numbers get conflated constantly, and conflating them is how a careful budget still overflows.

The featured models publish a context window of 1,050,000 tokens and a maximum input of 922,000 tokens. Those are different limits: the window covers the whole exchange, the input maximum is what your side of it may contain. If you size your trimming against the advertised context window, you have budgeted against a number that was never available to your messages, and the failure arrives at the boundary where the difference bites. Max output for the same models is published separately at 128,000 tokens.

While you are tuning sizes, note a documented cross-effect that surprises people coming from other vendors: on OpenAI, your rate limit is calculated as the maximum of max_tokens and the estimated tokens of the request. An inflated max_tokens you never actually reach still consumes your per-minute token allowance. It is not what produced this 400, but it is what produces the 429 you get a week after fixing this one.

Fix by which population you are in

  • Single oversized payload — do not trim it blindly from one end. Chunk it and process the pieces, or retrieve only the passages that matter and send those. Truncating a document at an arbitrary token boundary usually removes the part that answers the question, and the model cannot tell you it is missing.
  • Accumulating conversation — summarize the middle and keep the ends. The system message and the most recent turns carry the behaviour; the middle carries the facts, which compress. Do the summarization on a schedule, not on the error, so you are not doing surgery in an exception handler.
  • A handler that appends on failure — fix the handler first, before anything else. Every other change is masked by a loop that grows the payload.
  • Unexpected bulk in messages — strip tool results you no longer need after they have been used, and stop echoing entire retrieved documents back into the history. The most common single win is dropping stale tool output.
  • Genuinely need more room — move to a model with a larger window, and read the next section before you do.

The bill you can walk into while fixing this

Switching to a larger-context model is the fastest fix and the one with a documented cliff attached. Prompts over 272K input tokens are billed at 2× the input and cache rates and 1.5× the output rate for the full request — not for the overflow, for all of it. A request that lands just past the threshold costs roughly twice what the same request cost just before it.

That changes the economics of “just send everything”. If your fix is a bigger window plus no trimming, model the cost at your actual input size before you ship it, and keep the trimming you were about to delete. Cache writes are separately billed above the uncached input rate, so a large prefix you rewrite every request is expensive in a second way.

How to confirm it’s fixed

Measure, do not observe. A request succeeding once proves nothing for an accumulating conversation, because the failure is a function of turn count.

Assert the count before sending. Add a check that computes input tokens locally and compares against the model’s documented input maximum, minus the room you need for the reply. That assertion is the fix; the successful request is a side effect of it. Log the number on every call so you can see the trend rather than the crash.

Then run the long case. For a conversation, run past the turn count where it used to fail — at least a few turns beyond — and require the logged input count to stay flat or sawtooth rather than climb. A monotonically rising count means your trimming is not actually running, and you have bought yourself days, not a fix.

  • stream disconnected before completion: your input exceeds the context window is the same overflow arriving during a streamed request, where it reaches you as a stream event instead of a clean 400 — worth reading if your failure had already started producing output.
  • maxTokens is too close to contextLength is the opposite squeeze: the input fits but the requested output leaves no room. Same budget, different end of it.
  • Error running remote compact task: stream disconnected before completion is what happens when the automatic summarization meant to prevent this is itself the thing that fails.
  • The context window calculator does the arithmetic this page tells you to do locally: what fits, at what model, with how much left for the reply.