Ganz ehrlich: TTFB ist die undankbarste Zahl im Dashboard. Sie steht da, sagt 800 ms, und danach hört die Auskunft auf. War es die Datenbank? Ein Template, das zu viel rendert? Der Zahlungsanbieter, dessen API heute schlecht drauf ist? Der Cache, der nicht gegriffen hat? Die Zahl weiß es. Sie sagt es nur nicht.
Dabei ist die Antwort meistens einen Response-Header weit weg.
TTFB ist eine Summe, keine Diagnose
Time to First Byte misst die Spanne vom Klick bis zum ersten Byte der Antwort. Da steckt alles drin: DNS, Verbindungsaufbau, TLS, der Weg zur Edge, der Weg von der Edge zum Origin und dann die komplette Arbeit deiner Anwendung. Ein einziger Wert für ein halbes Dutzend Stationen.
Solange er klein ist, ist das egal. Wird er groß, wird jede Optimierung zur Wette. Man baut die Datenbank-Query um, deployt, wartet zwei Tage auf genug Daten, schaut. Keine Bewegung. Also das Template. Deployt, wartet, schaut. Immer noch nichts. Nach der dritten Runde stellt sich heraus: Es war der Suchindex, an den nie jemand gedacht hat.
Das ist kein Können-Problem, das ist ein Sichtbarkeits-Problem. Du optimierst eine Summe, ohne ihre Summanden zu kennen.
Der Header ist drei Zeilen im Backend
Server-Timing ist ein Standard-Response-Header, mit dem dein Backend an jede Antwort dranschreibt, wo die Zeit geblieben ist. Kein SDK, kein zusätzlicher Request, keine Instrumentierung im Browser. Du misst ohnehin schon irgendwo im Code, du schreibst die Werte nur noch in einen Header:
$response->headers->set('Server-Timing', sprintf(
'db;dur=%.1f, render;dur=%.1f, cache;dur=%.1f',
$dbMs, $renderMs, $cacheMs
));
Das ist Symfony, also auch die Welt von Shopware. Für Flask, Express und andere steht dasselbe in drei Zeilen in der Doku. Der Browser liest den Header ohnehin mit, und unser Beacon liest ihn bei jedem Seitenaufruf aus der Dokument-Antwort mit aus. Ab da ist die Aufschlüsselung Teil deiner Felddaten, gemessen an echten Besuchern statt an einem Testlauf.
Neun Phasen, und was hinter ihnen steckt
Damit aus fremden Metriknamen etwas Vergleichbares wird, normalisiert fastmon sie auf neun Phasen:
- Edge (
edge_dur) und Origin (origin_dur): der Weg. Zeit an der Edge und Zeit zwischen Edge und Ursprungsserver. - Backend (
backend_dur): die Anwendung insgesamt. Hier landet auch, was WordPress alswp-totalschickt. - Datenbank (
db_dur) und Rendering (render_dur): die zwei Klassiker, relationale Queries und das Bauen der Seite. - Cache (
cache_dur), Externe Aufrufe (external_dur), Suche (search_dur) und Key-Value-Store (kv_dur): die vier, die aufsummiert werden, weil pro Request mehrere davon vorkommen.
Die üblichen Namen kennt fastmon schon: elasticsearch, opensearch und solr landen in der Suche, redis und valkey im Key-Value-Store, http, fetch und api bei den externen Aufrufen. Wer es eindeutig will, schickt die Phasen direkt unter den fm--Namen, die haben Vorrang vor allem anderen.
Sichtbar wird das Ganze unter Analytics, Server-Timing, im Wasserfall der Response-Aufschlüsselung und als Spalte im Explorer, jeweils als p50, p75 und p95. Perzentile, nicht Durchschnitt, aus demselben Grund wie überall sonst: Der Durchschnitt schmeichelt, das p75 zeigt dir die Besucher, die es wirklich trifft.
Was du danach abliest
Der Moment, in dem sich die drei Zeilen bezahlt machen, sieht ungefähr so aus: Backend 60 ms, Datenbank 25 ms, Rendering 30 ms, Suche 240 ms. Die Diskussion über Query-Optimierung ist damit vorbei, bevor sie angefangen hat. Nicht die Datenbank ist langsam, der Suchindex ist es.
Oder: Alles unter 50 ms, nur externe Aufrufe stehen bei 300 ms. Dann liegt dein TTFB nicht an deinem Code, sondern an einem Dienst, den jemand vor zwei Monaten synchron in den Seitenaufbau gehängt hat.
Oder der unangenehme Fall: Cache-Phase hoch, Cache-Hit-Rate gut. Dann ist der Cache nicht das Problem, sondern die Lösung, die zu lange braucht, meistens weil er über das Netz statt über den lokalen Socket angesprochen wird.
Keiner der drei Fälle ist aus einer einzigen Zahl zu erkennen. Aus vier Zahlen springen sie dich in zehn Sekunden an.
Zwei neue Phasen, und eine Stufe im Chart
Suche und Key-Value-Store sind seit dem 27. August eigene Phasen. Vorher lag beides in cache_dur, was für Redis noch irgendwie stimmte und für Elasticsearch schon lange nicht mehr. Beides in einen Topf zu werfen, hieß: Man sieht, dass ein Store bremst, aber nicht welcher.
Das hat eine ehrliche Nebenwirkung, die du kennen solltest, bevor sie dir auffällt: Weil Redis aus der Cache-Phase in den Key-Value-Store umgezogen ist, wird cache_dur ab diesem Deploy kleiner. Im Chart ist das eine Stufe nach unten. Das ist keine Verbesserung, das ist eine Umsortierung. Die bereits gespeicherten Werte haben wir nicht angefasst, deshalb ist die Stufe da und bleibt sichtbar.
Early Hints: wenn dein TTFB plötzlich lügt
Und dann gibt es noch den Fall, in dem dein TTFB besser wird, ohne dass irgendetwas schneller geworden ist.
Mit 103 Early Hints schickt dein Server, meistens die Edge davor, vorab eine vorläufige Antwort mit preload- und preconnect-Hinweisen, während dein Backend noch arbeitet. Der Browser fängt schon mal an, Dateien zu holen. Für deine Besucher ist das ein echter Gewinn.
Für deine Messung ist es eine Falle: Das erste Byte, das ankommt, ist die 103, nicht deine eigentliche Antwort. Seit Chrome 133 zählt responseStart diese vorläufige Antwort mit, und weil fast alle Tools ihr TTFB daraus ableiten, fällt dein Wert nach dem Einschalten von Early Hints. Dein Server braucht exakt gleich lang.
Deshalb liest der fastmon-Tracker zusätzlich den Zeitpunkt der finalen Response-Header und legt ihn als ttfb_final daneben. Aus zwei Werten wird dann eine klare Auskunft:
- Beide gleich: Es gab keine vorläufige Antwort, dein TTFB ist dein TTFB.
ttfb_finalgrößer: Die Differenz ist genau der Vorsprung, den die Edge deinen Besuchern verschafft hat. Auf der TTFB-Seite steht er am gewählten Perzentil, in der Response-Aufschlüsselung als eigene Zeile vom ersten Byte bis zu den finalen Headern.- Leer: Der Browser meldet es nicht, oder es war eine Soft-Navigation.
Und daraus folgt die Faustregel, an der man sich sonst gern verrechnet: Edge- und Backend-Phase müssen in ttfb_final passen, nicht in ttfb. Wer gegen das kleinere der beiden rechnet, wundert sich über Phasen, die angeblich länger dauern als die ganze Antwort.
Hinweis: Welche Metriknamen auf welche Phase abgebildet werden, wie eigene Namen dazukommen und welche Grenzen für Werte und Beschreibungen gelten, steht vollständig in unserer Doku zu Server-Timing. Das ist die Seite für deine Entwickler.
Was du mitnimmst
TTFB sagt dir, dass es langsam war. Server-Timing sagt dir, wo. Der Unterschied zwischen beiden sind drei Zeilen in deinem Backend und danach eine Diskussion, die mit Zahlen statt mit Vermutungen geführt wird.
Fang mit db, render und cache an, das deckt die meisten Fälle ab. Wenn ein Suchindex oder ein Key-Value-Store im Spiel ist, nimm die zwei dazu, sie haben es sich verdient. Und wenn du Early Hints einsetzt, schau auf ttfb_final, bevor du dich über ein plötzlich hervorragendes TTFB freust.
Gemessen wird das alles an echten Besuchern, nicht im Labor. Mehr zu Real User Monitoring.