Skip to content
AI Infrastructure

Forty Words Over the Line, and the Server Threw Away Half My Prompt

A local inference server accepts a prompt right up to num_ctx. Exceed it by a little and it does not trim the overflow — it cuts the prompt to num_ctx/2 + 2 and returns no error. At an 8,192-token window that is 8,102 tokens accepted, then 4,098.

Published · How this was researched

Back to Global Tech Search

Market Horizon

Point-in-time measurement, Sep 2 2026 — one machine, three models, ollama 0.32.4

Target Sector

Local Inference, Edge AI, Measurement Methodology

Accepted at num_ctx 8192

8,102

Tokens, in full

Forty words later

4,098

num_ctx/2 + 2

Tokens lost

4,004

Not the overflow — half the window

Error returned

None

HTTP 200

At a context window of 8,192 tokens, this server accepted a 2,700-word prompt whole: 8,102 tokens, every one of them read.

I made the prompt forty words longer. It came back 4,098.

Not 8,192. Not a truncation of the overflow. Not an error — error: null, HTTP 200. Forty words past the line, and slightly more than half the prompt was gone.

The rule

Two things are true, and keeping them apart is the whole point of this post.

A prompt is accepted in full right up to num_ctx. Whatever the window is set to, the server will read a prompt of that size.

A prompt that exceeds num_ctx is cut to num_ctx/2 + 2. Not to num_ctx. Not to the last N tokens that fit. To half the window, plus two.

num_ctxLargest prompt accepted wholeNext size upTokens lost
4,0964,0662,0502,016
8,1928,1024,0984,004

That is the shape of the cliff. You do not pay for the overflow. You pay half the window.

The target reproduces everywhere

The truncation target itself is not model-specific. One oversized prompt, held byte-identical, sent at four window sizes across three models:

Modelnum_ctxAcceptednum_ctx/2
llama3.2:1b-instruct-q4_K_M204810261024
llama3.2:1b-instruct-q4_K_M409620502048
llama3.2:1b-instruct-q4_K_M819240984096
llama3.2:1b-instruct-q4_K_M1638481948192
qwen2.5:0.5b-instruct-q4_K_M204810261024
qwen2.5:0.5b-instruct-q4_K_M409620502048
qwen2.5:0.5b-instruct-q4_K_M819240984096
qwen2.5:0.5b-instruct-q4_K_M1638481948192
llama3.2:1b (Q8_0)204810261024
llama3.2:1b (Q8_0)409620502048
llama3.2:1b (Q8_0)819240984096
llama3.2:1b (Q8_0)1638481948192

Twelve cells, one offset. Two model families, two quantisations — so this is the serving layer, not a tokeniser. It also holds at 32,768 and 65,536, which accept 16,386 and 32,770.

Is anything actually lost?

A token count is not yet harm. A server that reported conservatively while reading the whole prompt would produce the same number.

The obvious test does not work. A 1.2B model failing to recall a nonce from 4,000 tokens of high-entropy filler may simply be a weak model, and comparing a truncated prompt against a short one confounds truncation with length.

So the control arm is length-matched: a prompt sized to land just under the truncation target, so both arms put roughly four thousand tokens in the window and differ only in whether the prompt was cut to get there.

ArmTokens in windowTruncatedBoth needles recovered
Fits4,066–4,069no3 of 3
Truncated4,098yes0 of 3

Under 1% apart in occupancy. Same model, same seed. Unclipped it returns HEAD CODE: alpha101, TAIL CODE: omega101 three times from three. Truncated it returns mangled fragments and finds neither.

Length is excluded. Capability is excluded. What is left is the truncation.

Which half goes

Knowing something was lost does not say what. For that I stopped asking the model to remember and started asking it to obey: one instruction at the very head demanding the single word RED, one at the very tail demanding BLUE.

Obeying a one-word instruction is something a 1.2B model does reliably, which is the point — here, obeyed neither is evidence of absence rather than of weakness. Head-only and tail-only run separately, because with both present the model prefers the more recent, and that recency preference is exactly what would masquerade as truncation.

ArmInstruction presentObeyed
Fitshead onlyhead, 3/3
Fitstail onlytail, 3/3
Fitsbothtail, 3/3
Truncatedhead onlyneither, 3/3
Truncatedtail onlytail, 3/3
Truncatedbothtail, 3/3

Unclipped, the head instruction is obeyed every time it is the only one present. Truncated, the identical instruction is obeyed never — while the tail instruction, in the same run, still works.

The half that gets thrown away is the beginning, which is where almost everyone puts the instructions.

Long prompts are assembled the same way nearly everywhere: system prompt and constraints first, then retrieved documents or history, then the user's actual question last. Cross the line and what you lose is the framing — the persona, the output format, the do not do X — while the raw material and the question survive to be answered without any of it.

The request succeeds. The model answers, in a plausible register, because the question is still there. It has simply stopped being told how.

Not parallel slots

A factor of exactly two has an obvious suspect: the KV cache split across concurrent request slots. It is a good hypothesis and it is wrong here, and the server says so itself.

OLLAMA_NUM_PARALLEL:1
llama-server ... -c 8192 -np 1 --context-shift --keep 4
llama_context: n_ctx      = 8192
llama_context: n_ctx_seq  = 8192

One slot. The whole window allocated to one sequence. And an oversized prompt still lands on half of it. What remains visible from outside is --context-shift --keep 4 on the same launch line; I did not read the source, so I will not tell you which line does it.

What to do about it

The practical rule is simpler than the one I published in the first draft of this post, and stricter than it looks.

Set num_ctx at least as large as your largest prompt. Not twice as large — that was my error, and it costs real memory for no reason. Just large enough.

Then verify, because the failure is silent. prompt_eval_count comes back on every response. Compare it against your own token estimate, and treat num_ctx/2 + 2 as a fire alarm: it means you crossed the line and the top of your prompt is gone.

if (res.prompt_eval_count === numCtx / 2 + 2) {
  throw new Error('prompt exceeded num_ctx; the head was discarded')
}

And if you cannot raise the window far enough on the memory you have — on 8 GB, the window itself costs 33 KiB per token, and disk is the real ceiling long before the model is — then put the instructions at the end. It is the opposite of how nearly every prompt template is written, and on this stack it is the half that survives.

Limits

One serving stack, one version, one machine. Three models is enough to show the target is not model-specific and nowhere near enough to show it is universal across serving stacks — I did not test llama.cpp directly, or vLLM, or LM Studio.

Everything here is black-box. The run establishes what the server does and rules out one explanation for it; it does not identify the code responsible.

One thing is genuinely unmeasurable from outside: the true token length of a prompt that got truncated. Once the server cuts it, it reports only what survived. That is why the overflow is quantified in words — forty of them — rather than in tokens.

The harness, the analyser, the four arms and the raw runs are in the repository. Six minutes and no credentials will tell you whether your build still does this.

Advantages

  • The truncation target reproduces in all twelve cells — three models across two families and two quantisations, four window sizes — so it is a property of the serving layer rather than of any model's tokeniser
  • The trigger is located by a fine sweep at two independent window sizes rather than inferred from oversized prompts, which is precisely the inference that made the first version of this measurement wrong
  • The loss is established by a length-matched control: both arms put ~4,000 tokens in the window and differ only in whether the prompt was truncated to get there, so prompt length and model capability are excluded by construction
  • The surviving end is identified by instruction-following rather than needle retrieval, a task a 1.2B model performs reliably, so 'obeyed neither' is evidence of absence rather than of weakness

× Challenges

  • One serving stack at one version. This is ollama 0.32.4 on an M1; the version is recorded in the data because a later build may not behave this way
  • Black-box throughout. The run establishes what the server does, not which line of upstream code does it, and does not claim to
  • The exact token length of a truncated prompt is unmeasurable from outside — once cut, the server reports only what survived — so the overflow is quantified in words, which is what can be controlled
  • No quality claim anywhere. Both retrieval arms ask only whether specific content was reachable, and neither scores an answer as good
Abhishek Kushwaha

Written by Abhishek Kushwaha

Full-stack software engineer in Kathmandu, Nepal — six years shipping production Django and Next.js systems, most recently at Pinakin Technologies & Research Center. Writes Global Tech Search on what the AI buildout costs in power, water and silicon, measuring it first-hand where he can. More about the author →