Capacity planning and cost per token
Turn traffic numbers into GPU counts with Little's law, check KV-cache memory, apply headroom and spares, and compute honest cost per million tokens including utilisation and prefill, with a worked end-to-end example and a script you can reuse.
Capacity planning answers one question: how many GPUs do I need for this traffic at this quality of service? Cost planning answers the follow-up: what does each token really cost me? Both start with the same measurements, and both are easy to get wrong in the same way: by planning from averages and single-user benchmarks.
#Step 1: describe the traffic
Measure or estimate, per request type (chat, summarise, extract, agent step):
- requests per second at peak (not average; plan for the busy hour and add a burst factor),
- average and p95 input tokens,
- average and p95 output tokens,
- the time-of-day pattern (a 9-to-5 service has 8 busy hours, not 24).
Do not guess. Instrument the prototype, or look at the logs of whatever the service replaces. If you have nothing, estimate with ranges and plan for the high end.
#Step 2: concurrency with Little's law
The number of requests in flight at once equals arrival rate times time in system:
concurrency = requests_per_second ร seconds_per_requestIf peak is 5 requests/s and a typical answer takes 8 s end to end, you need to support about 40 concurrent sequences. This number, not requests per second, determines your KV-cache memory and batch size (concurrency).
Seconds per request is TTFT + output_tokens / per-user decode speed. It is not fixed: it rises with load, which raises concurrency further. That feedback loop is why systems fall off a cliff near capacity.
#Step 3: check memory
Concurrency times context times KV bytes must fit alongside the weights:
KV memory = concurrent_sequences ร tokens_per_sequence ร kv_bytes_per_tokenWorked: 40 concurrent sequences, 3,000 tokens each (prompt plus answer), 128 KiB per token (an 8B model):
40 ร 3,000 ร 128 KiB = 14.6 GiBAdd 4.5 GiB of weights and about 1.5 GiB overhead: about 20.6 GiB. A 24 GiB GPU fits it with 14 percent headroom. If your tokens per sequence doubled, it would not. Quantizing the KV cache halves this (KV cache). If the memory does not fit on one GPU, concurrency per GPU is capped by memory, not compute, and you need more GPUs even if tokens/s would be enough.
#Step 4: measure throughput on your hardware
Use your own load test (load testing) with a realistic prompt mix and record aggregate output tokens per second at your target concurrency, plus the TTFT and per-user speed at that load. Do not trust a single-user benchmark: batching raises aggregate throughput while lowering per-user speed.
Example measurements for one GPU (illustrative):
| Concurrency | Aggregate tok/s | Per-user tok/s | p95 TTFT |
|---|---|---|---|
| 1 | 95 | 95 | 0.2 s |
| 8 | 520 | 65 | 0.5 s |
| 16 | 670 | 42 | 1.1 s |
| 32 | 700 | 22 | 4.8 s |
If your SLO says p95 TTFT under 1.5 s and at least 30 tokens/s per user, the usable capacity is around 12 concurrent sequences at about 600 tokens/s, not the 700 tokens/s at 32 users.
#Step 5: compute GPUs needed
Two checks, take the larger:
by throughput: tokens_needed_per_s = peak_requests_per_s ร avg_output_tokens
gpus_t = ceil(tokens_needed_per_s / (usable_tokens_per_s_per_gpu ร 0.7))
by concurrency: gpus_c = ceil(peak_concurrency / usable_concurrency_per_gpu)
gpus = max(gpus_t, gpus_c) + 1The 0.7 keeps you at 70 percent of the usable ceiling so spikes do not break your SLO. The + 1 is a spare for failures and rolling updates (N+1).
#End-to-end example
Traffic: 5 requests/s at peak, 300 average output tokens, 8 s per request. Measured usable capacity per GPU at your SLO: 600 tokens/s and 12 concurrent sequences.
tokens needed = 5 ร 300 = 1,500 tok/s
gpus_t = ceil(1,500 / (600 ร 0.7)) = ceil(1,500 / 420) = 4
peak concurrency = 5 ร 8 = 40
gpus_c = ceil(40 / (12 ร 0.7)) = ceil(40 / 8.4) = 5
gpus = max(4, 5) + 1 = 6The concurrency check dominates, which is typical when the SLO limits per-user speed. Six GPUs for 5 requests per second looks like a lot; the levers below can change that dramatically (a smaller or quantised model, a quantised KV cache, shorter prompts, caching, a faster GPU).
Your cost / 1M tokens
$0.56
at 40% busy ยท $0.22 at 100%
Monthly GPU bill
$1,460
2.63B tokens served
Break-even utilisation
37%
needed to beat the $0.60 API
Self-hosting is cheaper than this API by 7% at 40% utilisation.
cost per 1M = GPU $/hour รท (tokens/s ร 3,600 ร utilisation) ร 1,000,000. Excludes engineering time, redundancy and egress. Use your own measured throughput.
#Cost per million tokens
cost_per_M = gpu_cost_per_hour / (tokens_per_s ร 3600 ร utilisation) ร 1,000,000Utilisation is the lever people forget. A GPU that is busy 25 percent of the day costs four times more per token than the same GPU busy all day. The calculator above lets you see this.
Worked example (illustrative numbers). A GPU costs 1.50 per hour and delivers 600 usable tokens/s.
| Utilisation | Tokens per hour | Cost per million output tokens |
|---|---|---|
| 20% | 0.43 M | 3.47 |
| 40% | 0.86 M | 1.74 |
| 60% | 1.30 M | 1.16 |
| 80% | 1.73 M | 0.87 |
Notice the factor of four between 20 and 80 percent. Many teams discover that their expensive GPUs average 15 to 25 percent utilisation, because they sized for the peak hour and the rest of the day is quiet.
#Count input tokens too
Output tokens are the slow ones, but prompts also cost: prefill uses compute, and long prompts occupy memory. A request with 3,000 input and 300 output tokens is mostly prefill by token count, though prefill is far faster per token than decode. For planning, a simple honest approach is to cost per request from measurements:
cost_per_request = gpu_cost_per_hour / requests_served_per_hour_at_target_utilisationMeasure requests per hour in your load test with the real prompt mix, then divide. It includes everything.
#Ways to lower cost, in order of effort
- Raise utilisation: batch more requests, use continuous batching, schedule offline jobs into quiet hours, and scale replicas to the daily curve (autoscaling).
- Quantize: FP8 or 4-bit weights fit more concurrency per GPU, and a quantised KV cache fits more sequences. Verify quality with evals first (measuring quality).
- Cache: prefix caching removes repeated prefill (prompt caching).
- Shorten prompts and outputs: trim system prompts, retrieve fewer chunks, set sensible
max_tokens. - Right-size the model: route easy requests to a smaller model (gateways).
- Speculative decoding: faster per-user decode at low concurrency (speculative decoding).
- Cheaper hardware or pricing: reserved capacity for the baseline, spot for tolerant batch work (GPU choices).
#Compare against an API fairly
Hosted APIs price input and output tokens separately and usually quote per million. To compare, convert your numbers to the same split and include what you leave out of the GPU rent:
| Included in self-hosting cost | Often forgotten |
|---|---|
| GPU or instance hours | Idle and spare capacity (N+1, off-hours) |
| Storage and bandwidth | Engineering time to build and maintain |
| Monitoring and tooling | On-call burden |
| Power and hosting (owned hardware) | Evaluation, upgrades and security work |
Self-hosting usually wins when you have steady high volume, privacy needs or latency control, and loses at low or spiky volume. A rough break-even: take the monthly API bill, subtract what you would still need from the API (for example a frontier model for hard cases), and compare with the monthly self-hosting cost at your realistic utilisation, including people time.
#A reusable calculator
import math
def plan(peak_rps, avg_out_tokens, secs_per_req, usable_tps, usable_conc,
gpu_cost_hr, utilisation=0.5, headroom=0.7, spare=1):
tokens_needed = peak_rps * avg_out_tokens
concurrency = peak_rps * secs_per_req
gpus_t = math.ceil(tokens_needed / (usable_tps * headroom))
gpus_c = math.ceil(concurrency / (usable_conc * headroom))
gpus = max(gpus_t, gpus_c) + spare
cost_per_m = gpu_cost_hr / (usable_tps * 3600 * utilisation) * 1e6
monthly = gpus * gpu_cost_hr * 24 * 30
return dict(tokens_needed=tokens_needed, concurrency=concurrency,
gpus_by_tokens=gpus_t, gpus_by_concurrency=gpus_c, gpus=gpus,
cost_per_million_tokens=round(cost_per_m, 2), monthly_cost=round(monthly))
print(plan(5, 300, 8, 600, 12, 1.50))Change one input at a time and see what moves the answer: usually utilisation, per-GPU concurrency and tokens per request.
#Sensitivity: what to verify first
| Assumption | If wrong by 2ร | What to do |
|---|---|---|
| Peak requests per second | GPU count doubles | Measure real traffic, add a burst plan |
| Output tokens per request | Throughput check doubles | Log and cap output length |
| Time per request | Concurrency doubles | Measure under load, not at one user |
| Usable throughput at SLO | GPU count doubles | Load test with your prompt mix |
| Utilisation | Cost per token halves or doubles | Track it; scale to the daily curve |
Always recompute with your own measurements. Prices and hardware change quickly, so treat any example number, including the ones here, as a template.
#Common mistakes
- Planning for average traffic, then missing the SLO at peak.
- Using a single-user benchmark as the capacity number.
- Forgetting KV-cache memory, which caps concurrency before compute does.
- Ignoring utilisation when quoting cost per token.
- Comparing GPU rent to API price without people and idle costs.
- No spare: a single failure or rolling update causes an outage.
#Exercise
A service has a peak of 3 requests/s, 250 average output tokens, 10 s per request. One GPU measures 450 usable tokens/s and 10 concurrent sequences at your SLO, costs 2.00 per hour, and you expect 35 percent average utilisation.
- How many GPUs do you need with 70 percent headroom and one spare?
- What is the cost per million output tokens at 35 percent utilisation?
Answers:
- Tokens needed
3 ร 250 = 750;gpus_t = ceil(750 / (450 ร 0.7)) = ceil(2.38) = 3. Concurrency3 ร 10 = 30;gpus_c = ceil(30 / (10 ร 0.7)) = ceil(4.29) = 5. Somax(3, 5) + 1 = 6GPUs. Concurrency dominates. 2.00 / (450 ร 3,600 ร 0.35) ร 1,000,000 = 2.00 / 567,000 ร 1e6 โ 3.53per million output tokens. Raising utilisation to 70 percent would halve it, which is why scheduling and autoscaling matter.
Quick quiz
+10 XP per correct answer, +25 XP for a perfect score
1. A GPU costs $2/hour and serves 2,500 tokens/s at full load. Roughly what does 1M tokens cost at 100% utilisation?
2. If real utilisation is 25% instead of 100%, cost per token...
3. Peak traffic is 6 requests/s and each takes 10 s end to end. How many concurrent sequences must you support?
4. Why plan with a measured throughput at your target concurrency rather than a single-user benchmark?
5. What is the purpose of the 0.7 factor and the + 1 in the GPU-count formula?