Build1 publisher3 min readPublished
Ollama divides the whole prompt by the time it spent computing one token of it
The daemon's prompt_eval_count includes cache reads and prompt_eval_duration does not, so one qwen2.5:7b prompt reported 13,826 tokens a second and 43 tokens a second half a minute apart. One subtraction fixes it.
The Engineer · Build desk

What happened
- The same Ollama daemon reported 13,826 tokens a second and 43 tokens a second for the same prompt thirty seconds apart, on version 0.34.0 with qwen2.5:7b and nothing else running on the machine.
- The overstatement equals the cache hit ratio, count divided by uncached count, which came to 318x on a row where 317 of 318 prompt tokens were read from the KV cache.
- Moving a timestamp to the front of an otherwise identical 227-token prompt made prefill take 2.5 times as long as the version with the timestamp at the end.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A capacity plan sized on warm prefill rates overstates headroom by whatever the cache hit ratio happened to be during measurement, and a long stable system prompt pushes that ratio close to one.
- decision Prompt assembly order becomes an engineering decision with a measured price attached, so anything that changes per turn now has a reason to sit below the text that does not.
- exposure Anyone estimating load by summing input_tokens from the Anthropic-compatible endpoint undercounts every warm turn, because that field reports total minus cache reads.
- contradiction Ollama already computes the honest rate internally, so the correction is a client-side reading problem and not a request for the daemon to learn anything new.
Two fields in the same JSON response stopped sharing a unit. `prompt_eval_duration` times only the tokens the model actually computed [3]. `prompt_eval_count` still reports the whole prompt, cache reads included [4]. Divide the second by the first and the numerator and the denominator are describing different work.
Run the numbers on the warm row. 318 tokens at 13,826 tok/s implies a prefill duration of about 23 milliseconds [20]. One computed token in 23 milliseconds is roughly 43 tok/s, which is the figure the dev.to post calls the honest rate for that row [2]. The error is not a constant offset: it is count divided by uncached count, which the post puts at 318x with 317 of 318 tokens served from the KV cache [5][6].
I would not plan capacity on the 43 either. It is one token divided by one batch duration, so every fixed cost in that call lands on a single token. The only row where the two fields agree is the cold one, and the post reports that a cold prompt reads truthfully while a well-warmed one is off by the length of the system prompt [7].
On what that does to a graph, the post wrote that any dashboard plotting prefill tokens/sec over a conversation "is drawing a curve of its own cache hit rate and labelling it throughput" [8].
The fix is one subtraction: `uncached = count - (cached or 0)`, then divide by the seconds [10]. Ollama's own `Metrics.Summary()` does the same thing, so the daemon is internally consistent, and the older field is the one already sitting in everybody's code [11]. Two guards come with it. When uncached is zero there is no prefill rate, so print neither `0` nor infinity [12]. A fully cached prompt has no rate to report, which is inconvenient for a dashboard cell that expects a number. And daemons before 0.33.3 omit `prompt_eval_cached_count` altogether, so defaulting the missing field to zero converts "I don't know" into a confident claim that nothing was cached [9].
Reading the cached count also makes prompt layout measurable. Reuse survives only while the prompt matches from the very first token, and one volatile value near the top forfeits everything after it, every turn [14]. The post primed a 227-token body with a timestamp, then sent the same layout with a fresh timestamp; front placement cost 2.5 times the prefill time of the back-placed version [13]. The 40 tokens still reused in the front case were the chat template's own stable preamble, not any of the author's text [15]. Current date, session id, user name and per-turn retrieved chunks go below anything that stays put [16].
For the 318x to transfer, your prompt has to be almost entirely cache hits, which a long stable system prompt makes routine, and you have to be reading the total-including-cache field under whichever of the three API names your client uses [17]. The measurements are one author's, on one machine, on Ollama 0.34.0 with qwen2.5:7b [1]. The units mismatch travels to any machine; the 318 is a property of that prompt's hit ratio.
The author also flagged a day spent producing a confidently inverted result: the first attempt primed with one layout, sent the other, and read the hit count [18]. The post's measurements cover the prompt fields, and it does not examine the generation counters.
What to watch
- Whether Ollama renames or deprecates prompt_eval_count so old client code stops dividing a whole prompt by a partial duration.
- Whether the Anthropic-compatible endpoint's input_tokens semantics get documented, since reconstructing prompt size means adding cache reads back.
- Whether anyone reproduces the 2.5x front-versus-back placement penalty on other models and other Ollama versions.