n4nAI

Tokens per second benchmark: single request vs concurrent

Tokens per second single vs concurrent requests: how concurrency changes throughput, honest benchmark method, and which metric matters for LLM workloads.

n4n Team5 min read1,072 words

Audio narration

Coming soon — every post will get a voice note here.

The gap between tokens per second single vs concurrent requests is the single most misunderstood variable in LLM capacity planning. A provider that advertises 100 tokens/sec on a isolated benchmark may sustain half that per stream under real concurrent load, while still doubling aggregate output. You need to measure both axes before trusting any throughput ranking.

Why single-request tokens/sec lies to you

A single streaming request measures the best-case latency path: one sequence occupying a decoder with no contention for scheduling, memory bandwidth, or KV cache space. That number is real, but it describes a workload that does not exist in production unless you have exactly one user.

Autoregressive decoding is memory-bandwidth bound, not compute bound, for most practical model sizes. Each generated token requires a full weight read from HBM. With one stream, the GPU’s compute units sit idle while the memory bus delivers weights for a single batch dimension. The hardware is fundamentally underutilized.

When you launch a second request, the scheduler can often fuse the two decode steps into one larger matrix multiply. The arithmetic intensity rises, and the cost of moving weights is amortized across more sequences. This is why aggregate throughput climbs. But the individual streams now share that bus, so each sees lower tokens/sec than it did alone.

What concurrency actually changes

Batching and compute density

Transformers decode one token per step. The forward pass is a series of GEMMs against weight matrices. A single sequence gives batch size 1; the effective TFLOPS achieved is a fraction of peak. Concurrent requests let the serving stack (vLLM, TensorRT-LLM, or a provider’s custom router) pack many sequences into a single batch. Utilization improves until the batch saturates compute or memory capacity.

KV cache contention

Each active sequence caches its key/value tensors for every layer. That cache grows with sequence length and batch size. At some concurrency level, the cache pressure forces either smaller batches, eviction of older contexts, or swapping to host memory—all of which degrade per-stream speed. A 70B parameter model in fp16 already consumes ~140GB for weights alone; KV cache for long contexts is additional and can become the binding constraint.

Scheduling and provider limits

Managed endpoints enforce per-account rate limits and queue depth. A request that arrives while the batch is full waits in a pre-processing queue, inflating its wall-clock time without generating tokens. An OpenAI-compatible gateway such as n4n.ai, which fronts 240+ models with automatic fallback when a provider is degraded, can mask a single provider’s concurrency collapse if you don’t pin the route during a benchmark. Honor the client routing directive and disable fallback in your test harness to get clean numbers.

Benchmark methodology that doesn’t lie

Isolate the variable

Fix the model, the prompt length, the max tokens, and the decoding parameters (temperature, top_p). Run a single-request baseline, then run concurrency levels of 2, 4, 8, 16, 32. Use the same prompt text repeated to avoid variance from tokenization. If you care about long-context behavior, use a realistic system prompt and few-shot history.

Do not mix models mid-test. Different model architectures batch differently; comparing a 7B to a 70B under the same concurrency tells you nothing about either.

A minimal load generator

The script below fires N concurrent streaming requests against any OpenAI-compatible endpoint and reports both aggregate and per-stream tokens/sec. It uses the official openai python package; swap the base_url for your gateway.

import asyncio
import openai
import time

async def stream_one(client, prompt, max_tokens):
    start = time.monotonic()
    resp = await client.chat.completions.create(
        model="mistral-7b-instruct",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=max_tokens,
        stream=True,
    )
    tokens = 0
    async for chunk in resp:
        if chunk.choices[0].delta.content:
            tokens += 1
    dur = time.monotonic() - start
    return tokens, dur

async def main(concurrency, prompt, max_tokens=256):
    client = openai.AsyncOpenAI(
        base_url="https://api.your-gateway.com/v1",
        api_key="sk-your-key",
    )
    tasks = [stream_one(client, prompt, max_tokens) for _ in range(concurrency)]
    results = await asyncio.gather(*tasks)
    total_tok = sum(r[0] for r in results)
    wall = max(r[1] for r in results)  # batch completes when slowest finishes
    agg_tps = total_tok / wall
    avg_per = sum(r[0] / r[1] for r in results) / len(results)
    print(f"concurrency={concurrency} agg_tps={agg_tps:.1f} per_stream_tps={avg_per:.1f}")

asyncio.run(main(8, "Explain batch inference in three sentences."))

Run this against the same model from a machine with low network jitter. Repeat three times and take the median; tail latency at high concurrency is noisy.

Measure what matters

Define two metrics explicitly:

  • Aggregate tokens/sec = total tokens generated by all streams / wall-clock duration of the slowest stream.
  • Per-stream tokens/sec = tokens generated by one stream / its own duration.

The tokens per second single vs concurrent requests comparison only becomes meaningful when you plot both curves against concurrency on the x-axis.

Reading the numbers: aggregate vs per-stream

The aggregate curve typically rises steeply from concurrency 1 to some knee, then flattens. The per-stream curve declines from its single-request peak, often asymptotically approaching a floor determined by memory bandwidth per sequence.

If your aggregate plateaus at concurrency 4, the serving stack is already saturated for that model on that hardware. Pushing concurrency higher only increases queue delay and worsens per-stream latency without more output. If aggregate still climbs at 32, you have headroom and can scale request parallelism to reduce cost per token.

A common mistake is reporting only the aggregate. A bulk ingestion pipeline cares about aggregate, but a chat UI where a human waits on each token cares about the per-stream number at the concurrency your frontend actually produces.

Tradeoffs: latency vs throughput

Lower per-stream tokens/sec means higher time-to-first-token and slower character rendering. Users perceive “the model got dumb” when it is actually just contended. If your product is interactive, you must cap concurrency per model instance or provision enough replicas so that steady-state concurrency stays left of the knee.

Higher aggregate throughput at higher concurrency lowers dollar cost per million tokens because you amortize fixed inference overhead. Batch jobs, eval harnesses, and offline summarization should deliberately maximize concurrency up to the plateau.

The two goals conflict. You cannot have maximal per-stream speed and maximal aggregate throughput on the same instance at the same time. The decision is a capacity allocation problem, not a model quality problem.

When to care about tokens per second single vs concurrent requests

If you are building a single-user copilot or a CLI tool, the single-request number is your spec. Benchmark it, then add a 20% safety margin for background load.

If you are building a multi-tenant API, the concurrent numbers define your autoscaling policy. Track the aggregate plateau and set your max replicas so that (requests_in_flight / replicas) <= knee_concurrency.

If you are publishing a throughput ranking, report both. A table that lists only single-request tokens/sec is advertising, not engineering. A table that lists only aggregate at concurrency 64 is equally misleading for interactive use cases.

Takeaway

Always benchmark tokens per second single vs concurrent requests as a paired measurement, on the exact model and endpoint you will ship against. Use the single-request figure to set latency expectations for interactive paths; use the aggregate curve to size fleet capacity and predict cost. Treat any published LLM speed claim that omits concurrency level as incomplete, and pin your gateway routing during tests so fallback and provider variability do not corrupt the data.

Tagstokens-per-secondconcurrencybenchmark-methodology

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All tokens-per-second throughput rankings posts →