Skip to content

DeepSeek's max_tokens Default and Output Ceiling

8 min read · updated August 11, 2026

max_tokens is optional, which means every request you have ever sent without it used a default you did not choose. On DeepSeek that default differs by an order of magnitude between the two endpoints.

The documented figures

At the time of writing, DeepSeek documents these values on its models and pricing page, which is the page to check rather than to remember:

endpoint            max_tokens default   max_tokens maximum
deepseek-chat       4,096                8,192
deepseek-reasoner   32,768               65,536

Context window (input + output) is shared and much larger than
either ceiling; see the context-window page.

Four numbers, four independent things that can change. The default and the maximum are separate values, and neither is the context window. A model can accept 128K of input and still refuse to write more than 8,192 tokens of output, and that is not a contradiction — it is two different limits doing two different jobs.

Every figure in that table is a vendor-set limit that has moved between model versions and can move again without any change to your code. Read them from the pricing page before you size a job around them, and treat a finish_reason of length as the real signal rather than your recollection of the ceiling.

Why there is a default at all

A generation loop needs a termination condition. The model normally emits an end-of-sequence token, but nothing guarantees it will — repetition loops, degenerate sampling and adversarial prompts all produce output that would otherwise run until something else stopped it. max_tokens is that something else, and it is why an omitted value has to be filled in with a number rather than treated as unlimited.

The consequence for the chat endpoint is a familiar one: ask for a long document, omit max_tokens, and the answer stops mid-sentence at the default. Nothing failed. You accepted a 4,096-token budget by not setting one, and the model spent it.

Always set it explicitly. Not because the default is wrong, but because an explicit value is a decision you can find in your code and change, whereas a default is a decision that lives in someone else’s documentation and changes without telling you.

Why the reasoner's numbers are larger

Reasoning tokens count against max_tokens. The trace is output, it is billed as output, and it is bounded by the output ceiling — so the budget on a reasoning request is shared between thinking and answering, in that order, with thinking taking whatever it needs first.

That ordering produces the failure mode specific to this endpoint. A reasoning request with a chat-sized max_tokens can spend the entire budget on the trace and be cut off before it writes any answer. You get a 200 response with a populated reasoning_content, an empty content, and finish_reason: "length". It looks like the model refused; it ran out of room.

resp = client.chat.completions.create(
    model="deepseek-reasoner",
    messages=[{"role": "user", "content": hard_question}],
    max_tokens=512,          # far too small for a reasoning request
)
msg = resp.choices[0].message
print(resp.choices[0].finish_reason)   # length
print(len(msg.reasoning_content))      # large
print(repr(msg.content))               # '' or None

The larger documented default on the reasoning endpoint exists to make that outcome unlikely for a caller who set nothing. If you are setting the value yourself, size it for the trace and not for the answer you want — the billing page shows how lopsided that ratio typically is.

What truncation looks like

Truncation is not an error. The request succeeds, the response is well-formed, and the only indication is finish_reason.

  • Non-streaming: choices[0].finish_reason is length, and the content ends wherever the budget ran out — frequently mid-word, since the cut is at a token boundary rather than a linguistic one.
  • Streaming: the same value arrives on the final chunk with an empty delta. A client that renders deltas and ignores the last chunk shows the user a truncated answer with no indication anything is missing.
  • With JSON output: truncation produces syntactically invalid JSON, because the closing braces were never generated. This is the single most common cause of parse failures against JSON output mode, and the fix is a larger budget rather than a more forgiving parser.
  • With tool calls: a truncated function.arguments string is invalid JSON in a field your code is about to parse. Check finish_reason before parsing, not after.

Budgeting output against the window

The context window bounds input plus output together, and max_tokens is a reservation against it made at request time. A prompt that fits comfortably can be rejected because the completion budget you asked for pushes the total over the limit — which is why the overflow error quotes a requested figure larger than your prompt.

available_for_output = context_window - prompt_tokens - safety_margin
max_tokens = min(model_output_ceiling, available_for_output)

# safety_margin covers chat-template scaffolding and the gap between
# a local token count and the server's; a few hundred tokens is ample.

Two habits follow. Compute max_tokens rather than hard-coding it, so a long conversation degrades into shorter answers instead of 400s. And when you are close to the window, reduce max_tokens before you start dropping messages — it is the cheaper of the two adjustments and it is reversible on the next turn.

Recovering from a truncated answer

Truncation will happen — on a long document, on an unexpectedly verbose answer, on a reasoning request that thought harder than usual. Three strategies, and which one applies depends on what you were generating.

  • Retry with a larger budget. The simplest option, and the right one when you were nowhere near the ceiling. You pay for the discarded attempt, which is why it is worth setting a sensible budget up front rather than discovering it per request.
  • Continue from where it stopped. Append the truncated output as an assistant message and ask the model to carry on. This works for prose and works badly for anything with structure, because the model does not reliably resume mid-syntax. On the beta base URL DeepSeek exposes a prefix-completion mode designed for precisely this continuation, which is a stronger version of the same idea.
  • Split the work up front. If the output is legitimately larger than any ceiling — a translated book, a per-record transformation over thousands of rows — no budget solves it. Decompose into requests whose outputs are individually small, and you also get retries, parallelism and progress for free.

Whichever you choose, the branch has to exist. The single most common production bug in this area is code that reads choices[0].message.content and never looks at finish_reason, which means a truncated answer is indistinguishable from a complete one and reaches the user, the database or the next stage of the pipeline as though it were finished. Check the field before you use the content, every time.