Chapter 15 of 15 11 min intermediate

Cost, Latency & Caching

Making it affordable at player scale

Verified against a working setup on 2026-08-10

A model in your game costs nothing while you are testing alone. The number that matters is the one at ten thousand players, and it is worth calculating before you design around the feature rather than after.

Pick the cheapest model that holds the character

Per million tokens, verified 2026-08-10

ModelInput / outputWhere it fits
claude-haiku-4-5$1 / $5Ambient lines, short answers, high volume.
claude-sonnet-5$3 / $15The default for real conversation.
claude-opus-5$5 / $25Set-piece moments only.
Output tokens cost roughly five times input. A prompt that produces shorter replies saves more than a shorter prompt does — "answer in one or two sentences" is a cost control, not just a style note.

Do the arithmetic first

The shape of the calculation matters more than any particular number, because your own numbers will differ:

  1. 1
    Tokens per exchange

    System prompt + history + player line (input), plus the reply (output). A short NPC exchange is often ~600 in and ~80 out.

  2. 2
    Exchanges per session

    Be honest. Players who like a system use it far more than your average suggests.

  3. 3
    Sessions per player per month

    From your existing analytics, not a guess.

  4. 4
    Multiply by your player count

    Then multiply again by three, because engaged players are not average players.

The input side is where costs hide. Every turn resends the whole conversation, so a long chat costs far more per message at the end than at the start — input grows with history while the reply stays the same length.

Prompt caching: the biggest single lever

Your system prompt — the character, the rules, the world facts — is identical on every request and is usually most of your input tokens. Caching makes the API charge roughly a tenth for that portion on subsequent calls.

On your proxy
json
{
  "model": "claude-haiku-4-5",
  "max_tokens": 300,
  "system": [
    {
      "type": "text",
      "text": "<the long, unchanging character description>",
      "cache_control": { "type": "ephemeral" }
    }
  ],
  "messages": [{ "role": "user", "content": "Hello?" }]
}
RuleWhy
Caching matches on an exact prefixOne changed byte early invalidates everything after it.
Stable content first, volatile lastCharacter and rules before the player line, never after.
Never interpolate the player name or time into the system promptIt makes the prefix unique per player, so nothing is ever shared.
Short prompts will not cache at allThere is a minimum cacheable length; below it, nothing happens and no error is raised.
Putting "The time is 14:32" or the player's name at the top of the system prompt silently disables caching for every request. If your cache hit rate is zero, look for a value that changes per request sitting near the front.

Verify rather than assume: the API response reports how many tokens were written to and read from cache. If the read count stays at zero across repeated requests, something in your prefix is changing.

Cutting latency

  • Stream. The largest perceived win by far — see Streaming NPC Dialogue.
  • Use a faster model for lines that do not need brilliance.
  • Ask for less output. Generation time scales with reply length; a two-sentence cap is a latency fix.
  • Trim history. Keep the last few turns plus a short summary, not the whole conversation.
  • Start early. Fire the request when the player enters the trigger volume, not when they press the talk button.
That last one is nearly free and very effective. By the time a player walks up to an NPC and presses a button, a second of latency has already elapsed in the background.

Budget controls that belong on the server

All of these live on your proxy, because that is the only place a player cannot bypass them.

  • Per-player rate limits. One player should not be able to spend your monthly budget.
  • A global daily ceiling. When it trips, serve pre-written lines instead of erroring.
  • Spend alerts on the API account. Set these on day one, not after the first surprise.
  • Per-player usage logging. Without it you cannot tell abuse from enthusiasm.

Fallbacks are a feature, not error handling

The network fails, budgets trip, and requests occasionally get declined. Every one of those paths needs a line the NPC can say, and the line should be good enough that a player who never sees a generated response still enjoys the character.

C#
// Written by you, shipped with the game, works offline.
[SerializeField] private string[] fallbackLines =
{
    "Forge's cold. Come back later.",
    "Not much to say today.",
    "Mm. Busy.",
};

private string FallbackLine() =>
    fallbackLines[UnityEngine.Random.Range(0, fallbackLines.Length)];
The test is simple: turn off your wifi and play for ten minutes. If the game is merely less varied, your fallbacks are doing their job. If it feels broken, the model became load-bearing without you deciding that it should.

What to take away

  • Output tokens cost several times input — cap reply length.
  • Cost per message grows with conversation length, because history resends.
  • Cache the system prompt; keep everything volatile after it.
  • A per-request value near the front of the prompt silently kills caching.
  • Streaming and starting early buy more perceived speed than a faster model.
  • Rate limits, ceilings, and spend alerts belong on the server, from day one.
  • Ship fallback lines good enough to stand on their own.