Research · Build it

How much inference capacity does an AI game need?

At a fixed latency target, a pool serving 100 players runs at 56% utilisation and one serving 10,000 at 97%. Here is the queueing maths behind that, a simulation that checks it, and what it means for how AI games should buy compute.

Ammighty ResearchPublished 23 October 20265 min read

The question#

How much inference capacity does an AI-native game need to keep characters responsive? "Enough for peak" is the usual answer, and it hides the interesting part: how much headroom above average load you need depends heavily on how big the pool is. Queueing theory answers this exactly for a simple model, and a simulation tells us whether the simple model is safe to use.

Code: research/latency_queue.py. The workload numbers are assumptions; the method is the point.

The model#

Each active player sends a model request on average every seconds. Each request occupies one decode slot (a concurrent sequence in a continuously batched server) for a mean of seconds: prefill plus about 80 output tokens at ~40 tokens per second. With concurrent players, requests arrive at rate and the offered load in Erlangs is

so 1,000 players offer : on average that many slots are busy. Treating the pool as an M/M/c queue with slots, the probability that a request has to wait at all is the Erlang C formula, and the waiting time beyond that is exponential:

with . Our target: fewer than 1% of requests wait more than 200 ms in the queue before decoding starts. We search for the smallest that meets it. (Computing directly overflows beyond a few hundred slots. Our first version silently returned garbage at 10k players. The script uses the stable Erlang B recursion , then .)

Results#

Concurrent playersOffered load (Erlangs)Slots neededHeadroomUtilisationβ
10011.320+956%2.61
30033.847+1372%2.28
1,000112.5132+2085%1.84
3,000337.5364+2793%1.44
10,0001125.01,160+3597%1.04
30,0003375.03,416+4199%0.71
100,00011250.011,296+46100%0.43
Bigger pools run hotter at the same latency targetBigger pools run hotter at the same latency target50%60%70%80%90%100%1002005001k2k5k10k20k50k100kUtilisation at the latency targetConcurrent players in one shared pool (log scale)Slot utilisation

Utilisation of decode slots when sized for P(wait > 200 ms) < 1%.

A pool serving 100 players needs 20 slots for 11.25 Erlangs of load: 56% utilisation, so nearly half the capacity you pay for sits idle as insurance. At 10,000 players the same latency target runs at 97%. This is the square-root staffing law:

Headroom grows with , not , so relative headroom shrinks as pools grow. The fitted falls from 2.6 to 0.43 because the target is fixed in milliseconds while service times are seconds: in a large pool, a slot frees up every few milliseconds.

Is M/M/c safe to use?#

Real service times aren't exponential. Output lengths cluster, so service times vary less. We ran a discrete-event simulation with lognormal service times (log-sd 0.5, same mean), 300,000 requests per run, at each slot count Erlang C recommended:

PlayersSlotsErlang C: P(wait > 200 ms)SimulatedSimulated p99 wait
100200.59%0.58%69 ms
300470.63%0.52%103 ms
1,0001320.83%0.47%135 ms
3,0003640.96%0.45%139 ms

The simulation always beats the formula's prediction. Less variable service makes queues shorter, so Erlang C is conservative here: plan with it and you'll meet the target with a small margin. It would not be conservative if your output lengths were heavy-tailed (a character who occasionally monologues for 1,000 tokens). Cap max tokens per turn and the model stays honest.

What it means for a game#

  • Pool across titles and regions where latency allows. Ten games with 1,000 players each, sized separately, need 1,320 slots. One shared pool for 10,000 needs 1,160. That's 12% less capacity for the same latency. This is the economic argument for a shared runtime, and it's why Ammighty Runtime exists.
  • Small launches are the expensive ones per player. Early access with 300 concurrent players runs at 72% utilisation. Budget per-player inference cost at small scale accordingly, or use a shared pool from day one.
  • Shorter service time buys more than more slots. Halving (a smaller model, fewer output tokens) halves , and with it the slots needed. Combine with caching, which cuts prefill time as well as cost.

Limitations#

We model a slot as an independent server. On a real GPU, per-sequence throughput falls as the batch grows, so service time depends on load. Requests also aren't Poisson: scripted moments (a cutscene ending, a boss taunting) synchronise players, which creates bursts the model doesn't see. Use the formula for baseline sizing, then load-test with replayed telemetry. Finally, queueing delay is only one part of perceived latency; time-to-first-token and network round trip add to it. The LLM NPC handbook covers the full budget.

Reproduce it#

The full script is research/latency_queue.py (Python 3.10+, numpy, scipy; fixed seeds, so you'll get the same numbers). This is the core of it:

def erlang_c(c, a):
    b = 1.0
    for k in range(1, c + 1):          # stable Erlang B recursion
        b = a * b / (k + a * b)
    return c * b / (c - a * (1 - b))

def p_wait_gt(c, lam, mu, t):
    return erlang_c(c, lam / mu) * math.exp(-(c * mu - lam) * t)

c = floor(a) + 1
while p_wait_gt(c, lam, mu, 0.2) > 0.01:
    c += 1

Run it with python3 research/latency_queue.py; results land in research/out/, and the site's tables and charts are rendered from that JSON at build time. Swap in your own think time and service time, measured end to end on your serving stack under load, not from a single request. The discrete-event simulator des() in the same file lets you try other service-time distributions.

Frequently asked questions#

How do I estimate GPU capacity for LLM NPCs?

Compute offered load a = N·S/T (players × seconds per request ÷ seconds between requests), then use Erlang C to find the smallest slot count meeting your latency target. Validate with a load test.

What is square-root staffing?

A result from queueing theory: the capacity needed to hold a service level grows as c ≈ a + β√a, so headroom grows more slowly than load and large pools are more efficient.

Is Erlang C accurate for LLM inference?

It assumes exponential service times. When output lengths are capped and fairly consistent, it's conservative, as our simulation showed. Heavy-tailed outputs break that.

Got a game that talks back?

Send a build, a deck or two minutes of raw footage. A producer plays every pitch and answers within 10 business days.