Time to first token meten onder variërende serverbelasting
De reactiesnelheid van een taalmodelapplicatie staat of valt met de ervaren initiële wachttijd. De kernmetriek hiervoor is de Time to First Token (TTFT): de exacte tijdsduur tussen het moment waarop een client het HTTP-verzoek verzendt en het moment waarop de allereerste token via de Server-Sent Events (SSE) stream binnenkomt. Waar totale doorlooptijd (end-to-end latency) sterk afhankelijk is van het aantal gegenereerde uitvoertokens, weerspiegelt TTFT zuiver de combinatie van netwerklatentie, wachtrijvertraging op de server en de computationele verwerking van de invoerprompt. Met deze meting neem je concrete infrastructurele beslissingen over batching-algoritmen, capaciteitsplanning en fallback-strategieën naar alternatieve modelinstanties wanneer wachtrijen vollopen.
In dit artikel bakenen we de TTFT-meting expliciet af van algemene snelheidsstatistieken zoals gemiddelde doorvoer per seconde. Waar de algemene fundamenten worden beschreven in het overzicht over latency, doorvoer en tokens per seconde, richt deze gids zich specifiek op de dynamiek van streaming prefill-vertragingen onder concurrente systeemdruk. Een enkelvoudig verzoek op een rustige GPU geeft immers een vertekend beeld van de werkelijkheid. Pas wanneer meerdere parallelle gebruikers tegelijkertijd contextvensters van variërende omvang insturen, wordt zichtbaar hoe wachtrijbeheer en geheugentoewijzing de initiële wachttijd beïnvloeden.
De anatomie van Time to First Token
Om te begrijpen waarom TTFT explodeert onder belasting, moeten we de levenscyclus van een LLM-aanroep ontleden in afzonderlijke hardwarematige en softwarematige stappen. Een taalmodel verwerkt gegevens fundamenteel in twee fasen: de prefill-fase en de decoding-fase. Om te doorgronden welke matrixberekeningen tijdens deze fasen op de grafische processor plaatsvinden, verwijzen we naar het artikel waarin de technische werking van AI inference onder de motorkap stap voor stap wordt uitgewerkt. Tijdens de prefill-fase leest het model de gehele invoerprompt in één parallelle rekenstap in. Hierbij berekent de GPU alle sleutel- en waarde-vectoren (KV-cache) voor de context. Dit proces is sterk rekenkrachtgebonden (compute-bound).
De decoding-fase daarentegen genereert sequentieel één token per stap. Dit is voornamelijk geheugenbandbreedtegebonden (memory-bandwidth-bound). TTFT omvat het volledige netwerktraject heen, de tijd die het verzoek doorbrengt in de applicatiewachtrij van de serving engine, de prefill-berekening van de prompt en het genereren van de allereerste uitvoertoken. Zodra de server gelijktijdig honderden streams verwerkt, ontstaat er competitie om GPU-geheugen en compute-eenheden, waardoor de wachtrijtijd de dominante factor binnen de TTFT wordt.
Let op meetvalkuil: TTFT is géén constante eigenschap van een model. TTFT schaalt lineair tot kwadratisch met de invoerlengte (aantal input tokens) en degradeert niet-lineair zodra de serverconcurrency de verzadigingsgrens van het VRAM of de KV-cache pool overschrijdt.
Waarom gemiddelden falen: meten in percentielen
Het rapporteren van een gemiddelde TTFT over een testperiode maskeert structureel de incidentele pieken waar eindgebruikers tegenaan lopen. Wanneer een inference engine onder continue belasting draait, veroorzaken plotselinge pieken in concurrency of periodieke geheugenfragmentatie (deallocatie van KV-caches) zogeheten 'tail latency spikes'. Een gemiddelde TTFT van 400 milliseconden kan gepaard gaan met een 99e percentiel van 8 seconden, wat betekent dat één op de honderd interacties voor een menselijke gebruiker als een vastloper aanvoelt.
Bij betrouwbaar benchmarken kijken we daarom uitsluitend naar percentielverdelingen: p50 (mediaan), p90, p95 en p99. Wie dieper wil ingaan op de statistische verantwoording van deze verdelingen, kan de methodologie voor latency-percentielen meten in plaats van gemiddeldes raadplegen. Bij het benchmarken van TTFT onder variërende serverbelasting vergelijken we hoe de curve tussen p50 en p99 uit elkaar waaiert naarmate de virtuele gebruikersaantallen stijgen. Een gezonde architectuur toont een p99 die parallel meegroeit met de p50; een instabiele wachtrij vertoont een exponentieel weglopende p99 terwijl de p50 schijnbaar vlak blijft.
| Concurrente streams | p50 TTFT (ms) | p90 TTFT (ms) | p95 TTFT (ms) | p99 TTFT (ms) | Wachtrijstatus |
|---|---|---|---|---|---|
| 1 stream (basislijn) | 180 | 195 | 210 | 245 | Geen wachtrij (directe prefill) |
| 10 streams | 210 | 260 | 295 | 380 | Optimale continue batching |
| 25 streams | 290 | 480 | 620 | 1.150 | Lichte prefill-verdringing |
| 50 streams | 520 | 1.850 | 3.400 | 7.900 | KV-cache verzadiging & swaps |
Bovenstaande tabel toont fictieve getallen ter illustratie van het uitwaaierende percentieleffect bij overbelasting, geen absolute hardwarebenchmark.
Factoren die TTFT beïnvloeden tijdens piekbelasting
Wanneer de serverbelasting toeneemt, grijpen meerdere interne mechanismen van de serving engine (zoals vLLM, TensorRT-LLM of TGI) tegelijkertijd in. Om testresultaten zuiver te kunnen interpreteren, moeten we de vier belangrijkste oorzaken van TTFT-degradatie isoleren:
- Continue batching en chunked prefill: Moderne engines mixen prefill- en decoding-taken in dezelfde iteratiestap. Omdat prefill-operaties alle compute-kernen opeisen, splitsen engines grote prompts op in kleinere stukken (chunked prefill). Dit voorkomt dat lopende decoding-streams stotteren, maar verhoogt rechtstreeks de TTFT van de binnenkomende prompt.
- KV-cache geheugendruk en preëmptie: Zodra het videogeheugen vol raakt met actieve conversatiegeschiedenissen, moet de scheduler keuzes maken. Nieuwe verzoeken blijven in een wachtrij staan totdat eerdere streams zijn afgerond, of lopende verzoeken worden tijdelijk gepauzeerd (swapping/preemption), wat leidt tot extreme TTFT-uitschieters.
- Prompt caching hit-ratio: Wanneer meerdere verzoeken dezelfde systeemprompt of documentcontext delen, kan de engine eerdere KV-berekeningen hergebruiken. Een cache-hit verlaagt de TTFT met wel 80%, terwijl een cache-miss onder zware belasting de volledige prefill moet doorlopen.
- Netwerk- en TLS-handshakes: Bij het testen over openbare internetverbindingen kunnen TCP slow-start en TLS renegotiation tientallen milliseconden toevoegen die niets met modelprestaties te maken hebben.
Een reproduceerbare meetopzet inrichten
Een wetenschappelijk verantwoorde benchmark vereist strikte controle over variabelen. Als de invoerlengte willekeurig fluctueert tussen 50 en 4000 tokens, meet je geen serverdegradatie maar toevallige variantie in promptlengte. Voor een reproduceerbare TTFT-belastingstest hanteren we daarom een gestandaardiseerd protocol:
- Gefixeerde promptlengtes: Maak synthetische payloads met exact gedefinieerde token-aantallen (bijvoorbeeld 256, 1024 en 4096 tokens). Gebruik een vaste zaadwaarde voor willekeurige tekstgeneratie om contextuele consistentie te waarborgen.
- Gecontroleerde concurrency-trappen: Test niet met een willekeurige 'burst', maar voer de belasting stapsgewijs op. Start met 1 virtuele gebruiker (de onbelaste nulmeting), en verhoog in vaste stappen (bijv. 5, 10, 20, 50 gelijktijdige workers) met minimaal 3 minuten per trap om thermische throttling en steady-state geheugenbezetting te bereiken.
- Uitschakelen van client-side bottlenecking: Zorg dat het testscript asynchroon draait op een dedicated machine met voldoende netwerkbandbreedte en CPU-rekenkracht, zodat de client zelf geen vertraging introduceert bij het parsen van inkomende streaming chunks.
- Uitsluiten van client caching: Schakel HTTP keep-alive bewust in om werkelijke productiesituaties na te bootsen, maar varieer indien gewenst unieke identifiers om eventuele model-level prompt caches te omzeilen wanneer je de rauwe compute-capaciteit wilt testen.
Implementatie: TTFT benchmarken met Python en AsyncIO
Onderstaand script illustreert hoe je met Python, httpx en asyncio een gecontroleerde belastingstest uitvoert die specifiek de streaming tijd tot de eerste chunk registreert. Let op de exacte timing: we starten de klok direct vóór het verzenden van de HTTP POST-payload en stoppen de klok zodra de eerste non-empty regel uit de streaming response binnenkomt.
import asyncio
import time
import numpy as np
import httpx
API_URL = "http://localhost:8000/v1/chat/completions"
HEADERS = {"Authorization": "Bearer test-key", "Content-Type": "application/json"}
# Vaste prompt om ruis in prefill-berekeningen te voorkomen
PROMPT_PAYLOAD = {
"model": "meta-llama/Llama-3-8B-Instruct",
"messages": [
{"role": "system", "content": "Je bent een behulpzame assistent."},
{"role": "user", "content": "Schrijf een technisch essay van 500 woorden over netwerkprotocollen."}
],
"stream": True,
"max_tokens": 100,
"temperature": 0.0
}
async def measure_single_ttft(client: httpx.AsyncClient) -> float:
start_time = time.perf_counter()
first_token_time = None
try:
async with client.stream("POST", API_URL, json=PROMPT_PAYLOAD, headers=HEADERS, timeout=60.0) as response:
if response.status_code != 200:
return None
async for line in response.aiter_lines():
if line.startswith("data: ") and line.strip() != "data: [DONE]":
first_token_time = time.perf_counter()
break
except Exception:
return None
if first_token_time:
return (first_token_time - start_time) * 1000.0 # Milliseconden
return None
async def run_concurrency_tier(concurrency: int, total_requests: int):
limits = httpx.Limits(max_keepalive_connections=concurrency, max_connections=concurrency * 2)
async with httpx.AsyncClient(limits=limits) as client:
semaphore = asyncio.Semaphore(concurrency)
async def worker():
async with semaphore:
return await measure_single_ttft(client)
tasks = [worker() for _ in range(total_requests)]
results = await asyncio.gather(*tasks)
valid_ttfts = [r for r in results if r is not None]
if not valid_ttfts:
print(f"Concurrency {concurrency}: Alle requests gefaald.")
return
print(f"--- Concurrency Niveau: {concurrency} workers ({len(valid_ttfts)}/{total_requests} geslaagd) ---")
print(f"p50 TTFT: {np.percentile(valid_ttfts, 50):.1f} ms")
print(f"p90 TTFT: {np.percentile(valid_ttfts, 90):.1f} ms")
print(f"p95 TTFT: {np.percentile(valid_ttfts, 95):.1f} ms")
print(f"p99 TTFT: {np.percentile(valid_ttfts, 99):.1f} ms")
async def main():
for workers in [1, 5, 10, 25]:
await run_concurrency_tier(concurrency=workers, total_requests=workers * 10)
await asyncio.sleep(2)
if __name__ == "__main__":
asyncio.run(main())
De wisselwerking met payload-integriteit
Wanneer een LLM-server bezwijkt onder zware concurrency, degradeert vaak niet alleen de TTFT; ook de stabiliteit van de inference engine zelf komt onder druk te staan. Bij extreme wachtrijverdringing en plotseling afgebroken TCP-verbindingen zien we regelmatig dat streaming chunks incompleet raken of dat de interne parser van de server gecorrumpeerde uitvoer genereert. Wie strikte JSON-schema's afdwingt in productie, kan de evaluatiemethode voor JSON-opmaakvaliditeit bij hoge concurrency raadplegen om te verifiëren of het forceren van syntactische correctheid de TTFT extra vertraagt.
De prefill-fase voor JSON-geometrieën of uitgebreide tool-calling definities vereist immers extra grammaticavoorverwerking (zoals bij outlines of constrained sampling). Onder variërende serverbelasting vormt deze constrained decoding een dubbele belasting: de TTFT stijgt door de initiële schemacompilatie, terwijl de decoding-doorvoer stagneert doordat ongeldige token-logits per stap gemaskeerd moeten worden.
Nederlandstalige valkuilen bij TTFT-benchmarking
Bij het benchmarken van Nederlandstalige prompts tegen meertalige modellen stuiten we op een specifieke taalkundige anomalie: tokenizatie-inefficiëntie. Omdat de meeste tokenizers getraind zijn op overwegend Engelstalige corpora, worden Nederlandse woorden vaak opgeknipt in aanzienlijk meer subtokens per woord dan hun Engelse equivalenten. Samengestelde woorden zoals "aansprakelijkheidsverzekeringsmaatschappij" of veelvoorkomende werkwoordsvormen resulteren in een suboptimaal gefragmenteerde tokenreeks.
Dit heeft directe consequenties voor de TTFT onder serverbelasting. Een ogenschijnlijk identieke prompt van 300 Nederlandse woorden kan resulteren in 550 input tokens, terwijl de Engelse variant slechts 380 tokens telt. Tijdens de compute-gebonden prefill-fase moet de GPU hierdoor substantieel meer matrixvermenigvuldigingen uitvoeren voor exact dezelfde semantische lading. Wanneer tientallen concurrente gebruikers gelijktijdig Nederlandstalige prompts insturen, raakt de prefill-capaciteit van de server tot 40% sneller verzadigd dan bij Engelstalige prompts. Neem daarom in je benchmarkset altijd representatieve Nederlandse bedrijfsteksten op in plaats van direct vertaalde korte Engelse zinnen.
Kosten, doorlooptijd en capaciteitsgrenzen
Het structureel uitvoeren van grootschalige belastingstests brengt aanzienlijke operationele kosten met zich mee, zowel in API-tegoed als in benodigde testtijd. Het genereren van 10.000 testverzoeken met contextvensters van 2048 tokens jaagt miljoenen tokens door de pipeline. Om budgettaire verrassingen te voorkomen, is het raadzaam vooraf te berekenen hoeveel requests strikt noodzakelijk zijn om statistisch betrouwbare percentielen te berekenen.
Wanneer commerciële API-gateways worden getest, treedt bovendien vaak externe rate limiting op voordat de hardwarematige TTFT-grens van het onderliggende cluster is bereikt. Om te begrijpen hoe tussenliggende proxies verzoeken afknijpen via algoritmes zoals leaky bucket of token bucket, bekijken we hoe men een token bucket toepast in een LLM-gateway om verkeersstromen af te vlakken. Een goede belastingstest meet uitsluitend de pure reactietijd van de inference nodes door rate limits van tussenlagen expliciet te monitoren en te loggen via HTTP 429-foutcodes.
Conclusie en checklist voor betrouwbare TTFT-metingen
Het meten van TTFT onder wisselende systeembelasting legt genadeloos bloot hoe een AI-architectuur zich gedraagt wanneer het er écht toe doet. Door gemiddelden resoluut te verwerpen en te sturen op p95- en p99-percentielen, ontdek je tijdig waar prefill-knelpunten, geheugenfragmentatie en wachtrijophopingen ontstaan. Pas de volgende vaste stappen toe bij elke nieuwe benchmarkcyclus:
- Isoleer TTFT van totale doorlooptijd door expliciet te luisteren naar het eerste streaming-pakket via Server-Sent Events.
- Hanteer identieke, gefixeerde promptgroottes om variantie in computationele prefill-duur uit te sluiten.
- Bouw de belasting stapsgewijs op via duidelijke concurrency-trappen en meet per trap minimaal enkele minuten.
- Rapporteer altijd p50, p90, p95 en p99 naast het percentage gefaalde of getime-outte verbindingen.
- Houd rekening met Nederlandse tokenizatiescheefheid bij het dimensioneren van de invoerpayloads.
Met deze meetmethode transformeer je oppervlakkige snelheidsclaims naar harde, reproduceerbare data, waarmee je gefundeerde beslissingen neemt over hardware-inzet, serving frameworks en schaalbaarheidsgrenzen.


