Gemma-4-31B TTFT on shared gateway (~400ms floor) vs self-hosted (28ms), same checkpoint

Hi,

We’re building voice agents on livekit-agents (1.6.5) with google/gemma-4-31b-it via LiveKit Inference. The launch post advertises 192ms TTFT, but on the shared gateway (agent-gateway.livekit.cloud) we’re seeing about a 400ms floor, rising toward a second at realistic voice context sizes. To figure out how much is the model and how much is the serving path, we served the identical checkpoint ourselves on one H100 with stock vLLM. Numbers, recipe, and questions are below. Happy to share all the probe scripts.

How we measured: TTFT is the time from sending the request to the first SSE content chunk, with stream=true. Single warm connection, no concurrency, no retries. Network isn’t the bottleneck, since TCP connect to the edge is about 12ms. For cache probes we prepend a unique nonce (guaranteed cold) then send the byte-identical prompt again, which should hit.

Shared gateway vs self-hosted (median TTFT):

Tiny ~30-token prompt (warm floor): ~390ms vs 28ms
Fresh 1K prompt: ~500ms vs 130ms
Fresh 5.5K cold prefill: ~865ms vs ~864ms (identical, so pure compute, which is a nice sanity check)
Repeated 9K prefix: ~720ms vs 67ms (the cache actually hits when self-hosted)
50 concurrent voice sessions on the one H100: p50 73ms, p95 113ms, zero errors

Two things stand out. There’s a floor of roughly 350 to 400ms even on a tiny warm request, and with only 12ms of network in the path that looks like internal routing or queueing rather than compute. The launch post itself says warm requests start returning tokens in about 100ms. Second, prefix caching barely helps. Repeating a 9K prompt saves only about 100ms, which suggests requests land on different replicas with no session affinity. For a voice agent, the system prompt plus growing history is a stable prefix that just gets re-prefilled nearly every turn.

To be clear, this is only about latency. Quality was excellent on both endpoints. Our full 734-case eval covering tool calling, multilingual turns, and digit collection passed at 99.86 percent on each.

Our self-hosted recipe (vLLM 0.25.1, H100 NVL, agent co-located):

VLLM_USE_FLASHINFER_SAMPLER=0 \
vllm serve google/gemma-4-31B-it \
  --dtype bfloat16 --quantization fp8 \
  --enable-prefix-caching --enable-chunked-prefill \
  --max-model-len 32768 --gpu-memory-utilization 0.90 --max-num-seqs 64 \
  --tool-call-parser gemma4 --enable-auto-tool-choice \
  --limit-mm-per-prompt '{"image":0}' --api-server-count 4

A few things we learned the hard way:

FP8 applied at serve time only, from the BF16 checkpoint. We never load pre-quantized weights. It was quality-neutral for us and mainly buys prefill speed.

Don’t pass --reasoning-parser gemma4. With it on, confirmation and correction turns occasionally ran past the end of the turn and fired a premature confirm tool call, so the task never completed. Dropping it fixed this completely, and tool calling still works with just the tool parser plus auto choice. Same on 0.24.0 and 0.25.1.

Set max_tokens to at least 256 on tool-call turns. At 100, a turn with two corrections at once emits two parallel calls, the second truncates mid-JSON, and the args come back full of schema placeholders. Threshold is around 192. We saw this on both endpoints.

The limit-mm-per-prompt and api-server-count 4 flags are optional. They only helped about 25 percent on synchronized bursts.

FLASHINFER_SAMPLER=0 because our box has no nvcc, and the default sampler tries to JIT a kernel at startup, which kills engine init.

Keep the system prompt byte-stable and send one warmup request at deploy time. That’s where the 67ms comes from.

Questions:

  1. Is 192ms achievable on the shared tier, or is it specific to dedicated deployments? If dedicated, what’s the path to that (docs or pricing)?
  2. Are there plans for session affinity or cross-turn prefix reuse on the shared gateway? For voice this is the single biggest lever, since re-prefilling costs 500 to 900ms per turn at realistic sizes.
  3. Is there any way today to pin a region or replica per session, maybe via headers, that we missed?
  4. Is the 350 to 400ms floor on tiny warm requests expected for the shared tier?

Charts attached: TTFT comparison, the prefix-caching comparison, and per-turn TTFT for the 50 concurrent sessions. Probe scripts are single-file Python, happy to post.

Thanks!

Hi, thank you for the detailed analysis, can you share more about how you deployed the LiveKit cloud agents used to test? These should be hosted on LiveKit cloud in the US for best performance, since that is where we currently host the gemma model.

If you aren’t hitting the gateway from Agents hosted in LiveKit cloud, that would affect the latency numbers as well as possibly the prefix caching results you see. The reason I ask is because I don’t see any sessions served from the account tied to your login.

Hey, thanks for getting back to me.

So to clarify our setup: our agents don’t run on LiveKit Cloud. We run livekit-agents (1.6.5) workers on our own infrastructure, and the model side is your hosted gemma-4-31b through the inference gateway (agent-gateway.livekit.cloud/v1) with a JWT that has inference grants. So we’re using LiveKit Inference for the model, just not LiveKit Cloud for the agents themselves. We never create rooms or sessions, which would be why you’re not seeing any on our account. Everything in my post was measured on that path, and that’s also how we run in production, so the latency there is what actually matters to us.

About the client location thing, fair point, so I re-ran the same probes from a US datacenter box instead of our office machine. It’s about 45 ms from the gateway edge. The warm floor did improve, roughly 390 ms down to 287 ms, but the caching behavior didn’t budge: a 9K token prompt was 564 ms cold and 584 ms when I sent the exact same prompt again. So being closer helps a bit with the floor, but requests coming from outside your cloud still don’t seem to get any session affinity, and the conversation prefix gets re-prefilled on every single turn.

Which leaves me with two questions really. Is there any way for callers outside LiveKit Cloud to get session affinity, like a header or the user field or sticky routing, so the turns of one conversation can hit the same replica and reuse the prefix cache? And if that’s not possible, is dedicated inference something we could get while keeping our agents on our own infra? That seems like the path to the 192 ms number and I’d love to know what access looks like.

Happy to share the probe scripts if you want to reproduce any of this. Thanks again!

Thanks, just to make sure I understand correctly. You are self-hosting your LiveKit agents but not using LiveKit cloud at all, even for the communication to the model. Instead you are signing a JWT using your LiveKit cloud credentials and integrating directly with LiveKit inference endpoint at agent-gateway.livekit.cloud/v1?

If so, I’m not aware that we have done much testing with this scenario since the expectation is to use LiveKit Cloud for (at least) transport if you are using LiveKit Inference.

Which leaves me with two questions really. Is there any way for callers outside LiveKit Cloud to get session affinity, like a header or the user field or sticky routing, so the turns of one conversation can hit the same replica and reuse the prefix cache? And if that’s not possible, is dedicated inference something we could get while keeping our agents on our own infra?

I would need to double-check with engineering, but I just wanted to make sure I understood your setup first.

Yes you have got the setup correctly!

Hey @AI_Tools thanks for the thorough write-up. Right now we do our routing based on the room id (proper prefix trie routing will follow). So right now, the speed benefits will be limited to live agent sessions. Were your tests running in a livekit room or done by directly calling the completions endpoint?

calling the endpoints directly, yes