Server-Timing-Header
Sende Server-Timing-Header aus deinem Backend, und fastmon zerlegt deine Serverzeit in Phasen, Cache-Status und deine eigenen Custom-Keys.
TTFB sagt dir, dass der Server langsam war. Es sagt dir nicht, welcher Teil langsam war: die Datenbank, ein Third-Party-Call, das Template-Rendering oder einfach ein Cache-Miss an der Edge. Der Server-Timing-Response-Header ist der Standardweg, mit dem dein Backend genau diese Aufschlüsselung an die Response selbst anhängt, und der Browser stellt sie JavaScript zur Verfügung. Fastmon liest ihn bei jedem Pageview mit.
Neu: wird bereits erfasst
Die Server-Timing-Erfassung ist live: Der Beacon liest den Header bereits, jeder Beacon trägt ihn, serverseitig voll normalisiert. Die Analytics-Dashboards darüber werden noch optimiert, Panels und Gruppierungen können sich also noch verschieben, während wir das Layout finalisieren. Nichts, was du jetzt einrichtest, ist umsonst; die Daten werden schon heute erfasst.
Was fastmon liest
Der Header wird mit deiner Dokument-Response ausgeliefert, wie jeder andere Backend-Header:
HTTP/2 200
Content-Type: text/html
Server-Timing: edge;desc="HIT", db;dur=53, app;dur=47.2, render;dur=12.4Der Browser parst ihn in das serverTiming-Array des Navigation-Entries, und der Beacon kopiert jeden Eintrag aus dem Navigation-Timing des Hauptdokuments. Pro Eintrag liest er drei Dinge, genau so, wie die Spezifikation sie definiert:
| Feld | Quelle | Was es ist |
|---|---|---|
name | der Metrikname | db, cache, app, …: das Label, das du wählst |
dur | dur= | die gemeldete Dauer in Millisekunden (optional) |
desc | desc= | der Freitext (optional): z. B. ein Cache-Status |
Das ist per Konstruktion First-Party: Das einzige Server-Timing, das der Beacon sehen kann, ist das, das dein eigener Origin auf dein eigenes Dokument gesetzt hat. Es gibt nichts im Beacon zu aktivieren, und es wird nie ein Third-Party-Header gelesen. Erfasst wird es unter jeder Tracker-Einstellung. Es trägt keine Besucherinformation, nur die Timing-Werte deines Servers.
Wie fastmon die Werte normalisiert
Der Beacon schickt die rohen Einträge; die Leitung ist fälschbar, also wird der Filterung des Trackers nicht vertraut. Alles Folgende wird serverseitig erneut angewendet, bevor auch nur ein Wert gespeichert wird. Schlechte Einträge werden einzeln verworfen. Wegen einer fehlerhaften Metrik geht nie der ganze Beacon verloren.
Formregeln. Eine Metrik wird nur behalten, wenn sie besteht:
- Name: kleingeschrieben, muss
^[a-z][a-z0-9._-]{0,31}$matchen (beginnt mit Buchstabe, ≤ 32 Zeichen). Eintideways.layer.-Präfix wird zuvor entfernt. - Dauer: numerisch und
0 ≤ dur ≤ 10.000.000 ms. Alles darüber wertet fastmon als fälschlich übergebenen Zeitstempel und verwirft es. Ein rein numerischesdesc(z. B.items;desc=3) wird als Dauer interpretiert, aber nur, wenn es in dasselbe Fenster passt; eine Zahl außerhalb (Shopifystheme;desc="190311530496"ist eine Theme-ID) bleibt eine Description, statt zu einer sechsjährigen Dauer zu werden. - Description: ≤ 32 Zeichen, nur
[a-zA-Z0-9 _.:/-]; sonst verworfen. - Denylist & ID-Schutz: Infrastruktur- und Trace-Keys (
requestid,traceparent,cfray,servedby,asn,country,ipv6) und Konfigurationswerte, die keine Timings sind (Shopifyscompressionlevelundspeculationtags), werden direkt verworfen, und jeder Name oder jede Description, die wie eine UUID oder ein langer Hex-/Base64-Blob aussieht, wird als Kardinalitäts-/PII-Schutz fallengelassen. Die CDN-PoP-Keys (pop,fastly-pop,cdn-pop,x-qc-pop) bleiben: Sie benennen den ausliefernden Knoten, nicht den Besucher.
Drei Tiers. Was überlebt, wird einsortiert in:
- Hochgestufte Skalare (immer behalten): die acht Backend-Phasen, die zwei Cache-Status, den Seitentyp und die zwei Label-Dimensionen (
origin_host,logged_in). Sie werden zu dedizierten Spalten. - Tier-2-Katalog (ohne Cap): ein bekannter Satz von Namen (
kafka,queue,dns,cpu,auth,gc,parse,session,theme,twig,apcu,compressiontimesowie die Mitglieder der summierten Phasen unten) landet in den Dauer-/Description-Maps des Pageviews. - Tier-3-Custom (≤ 8 pro Pageview): jeder andere gültige Name, den du dir ausdenkst.
Über beide Maps hinweg sind Dauern auf 24 Keys und Descriptions auf 8 gekappt.
Fastmons eigene Metriken gewinnen
Wenn dieselbe Phase von mehr als einer Metrik gefüllt werden könnte, löst fastmon das in fester Reihenfolge auf:
fm-*First-Party-Alias › Site-Mapping › eingebauter Vendor-Alias (cf…) › eingebauter generischer Alias
Fastmons Edge injiziert eigene, mit fm- präfixierte Metriken (fm-edge, fm-origin, fm-backend, fm-db, fm-render, fm-http, fm-search, fm-kv, fm-cdn-cache, fm-origin-cache, fm-pagetype). Ist eine fm--Metrik vorhanden, gewinnt sie, weil sie an fastmons eigener Edge gemessen statt von der Seite gemeldet wird. Sie ist nicht clientseitig fälschbar und über jede Site hinweg konsistent. Darunter kommt ein Name, den du in deinen Site-Einstellungen gemappt hast, dann die eingebauten Aliase. Die Tabellen unten listen jeden erkannten Namen in genau dieser Vorrang-Reihenfolge, von links nach rechts. Ein Vendor-Name wie cfedge wird also dem generischen edge vorgezogen, und der erste, der eine Dauer trägt, gewinnt. Deine eigenen Metriken füllen die Phasen, die fastmons eigene nicht abdecken.
Backend-Phasen
Erkannte Namen werden auf acht Phasen abgebildet, gezeigt als p50 / p75 / p95 über den Zeitraum, jeweils mit der Zahl der Pageviews, die die Phase gemeldet haben. Vier nehmen den ersten Alias, der eine Dauer trägt; http, search und kv summieren jedes passende Mitglied; db nimmt den ersten relationalen Alias und addiert die Document-Stores obendrauf. Summierte Mitglieder bleiben für den Drill-down zusätzlich in den Maps. Der fm-*-Alias hat immer Vorrang.
| Phase | Erkannte Namen (in Vorrang-Reihenfolge) | Wie |
|---|---|---|
edge_dur | fm-edge, cfedge, edge, time-elapsed | erster mit dur |
origin_dur | fm-origin, cforigin, origin, cdn-upstream-fbl | erster mit dur |
backend_dur | fm-backend, processing, total, app, be, wp-total, bootstrap | erster mit dur |
db_dur | fm-db, db, sql, rdbms; dazu mongodb, sqlite | erster mit dur, + Summe |
render_dur | fm-render, render, wp-template, view, ssr | erster mit dur |
http_dur | fm-http, http, fetch, api, ext | Summe |
search_dur | fm-search, elasticsearch, opensearch, solr, search | Summe |
kv_dur | fm-kv, redis, valkey, kv, memcache, cache | Summe |
Phasen, die du nicht sendest, fehlen schlicht (NULLs werden übersprungen, nie als Null gezählt). Die Perzentile zeigen dir z. B., dass db dein p95-Problem ist, während render flach bleibt. Lies sie zusammen mit ihrem Count: Ein p75 über 12 Pageviews sagt weniger als einer über 50.000.
Was wohin zählt. db_dur ist der Primärspeicher. Die relationalen Aliase sind verschiedene Namen für dieselbe Messung, also gewinnt der erste; mongodb und sqlite werden addiert, weil ein Document-Store dort, wo er auftaucht, der Primärspeicher ist (Tideways meldet rdbms und mongodb als getrennte Layer eines Requests). Suchmaschinen zählen nie als Datenbankzeit. kv_dur ist die gesamte Key-Value- und Cache-Schicht, egal welche Engine dahintersteht: Ein Redis-Object-Cache und derselbe Cache auf memcache landen in derselben Spalte, und der Drill-down pro Key sagt weiterhin, welche Engine die Zeit gebraucht hat. apcu (Shared Memory im Prozess) und Shopifys edge_cart stecken in keiner Summe und bleiben in den Maps. Queue-Broker (amqp, kafka, beanstalk, queue) bleiben ebenfalls nur im Drill-down: Synchrone Broker-Zeit in einem Request ist fast immer ein billiges Publish.
Alle acht Phasen erscheinen auf der Server-Timing-Seite, im Wasserfall der Response-Aufschlüsselung und als Metriken und Spalten im Explorer (die KV-Phase heißt „KV / Cache"; der Full-Page-Cache ist der Cache-Status des Origins, eine andere Schicht). Der Pageview-Drawer listet die Phasen verschachtelt: DB, Search, KV / Cache, HTTP und Render unter Backend, Backend unter Origin, jede mit ihrem Anteil an der Zeile, die sie enthält, und die keiner Schicht zugeordnete Zeit als Zeile „Nicht zugeordnet".
Cache-Status
Ein Cache-Urteil wird als Description übertragen, nicht als Dauer: z. B. Server-Timing: cdn-cache;desc="HIT". Fastmon erfasst zwei Schichten unabhängig:
| Schicht | Erkannte Namen (in Vorrang-Reihenfolge) |
|---|---|
cdn | fm-cdn-cache, fm-cdn, cfcachestatus, cdn-cache, hit-state |
origin | fm-origin-cache, fpc, x-cache, cache |
Die bloßen Flags von CloudFront werden als Fallback für die CDN-Schicht erkannt: cdn-cache-hit → HIT, cdn-cache-miss → MISS, cdn-cache-refresh → REVALIDATED (ein explizites CDN-Key überschreibt sie weiterhin).
fm-fpc ist weg
Der Key für den Origin-Status heißt fm-origin-cache. fm-fpc war die alte Schreibweise und wird nicht mehr erkannt: es war der einzige First-Party-Key, der die Origin-Ebene anders nannte als origin, und hätte neben fm-origin-age zwei Wörter für eine Ebene in einem Header bedeutet. Ein Header, der weiterhin fm-fpc sendet, erscheint in der Top-Keys-Liste des Panels, statt stillschweigend nichts zu füllen: der Fehler ist also sichtbar. fm-cdn funktioniert weiterhin, fm-cdn-cache ist die dokumentierte Schreibweise.
Der Wert wird großgeschrieben und auf einen der Status normalisiert, die fastmon speichert. Fastlys zusammengesetzte Formen kollabieren zuerst (MISS-CLUSTER → MISS, HITPASS → PASS, HIT-STALE → STALE); alles außerhalb der Liste unten wird unverändert durchgereicht, gekappt auf 12 Zeichen, sodass ein herstellerspezifischer Status nie stillschweigend verloren geht.
| Status | Bedeutung |
|---|---|
HIT | Aus dem Cache ausgeliefert, kein Origin-Abruf. |
MISS | Nicht im Cache; vom Origin geholt (und meist für das nächste Mal gespeichert). |
EXPIRED | Eine gecachte Kopie war abgelaufen und wurde vom Origin aufgefrischt. |
STALE | Eine veraltete Kopie wurde ausgeliefert (stale-while-revalidate oder Origin nicht erreichbar). |
REVALIDATED | Die gecachte Kopie wurde gegen den Origin geprüft und als noch frisch bestätigt. |
UPDATING | Eine veraltete Kopie wurde ausgeliefert, während der Cache sie im Hintergrund auffrischte. |
BYPASS | Der Cache wurde für diese Anfrage bewusst übersprungen; der Origin lieferte aus. |
DYNAMIC | Als dynamisch und nicht cachebar behandelt; immer vom Origin. |
PASS | Ungecacht durchgereicht (Fastly pass; HITPASS kollabiert hierher). |
NONE | Kein Caching angewandt oder konfiguriert. |
Beide Schichten nutzen dasselbe Vokabular. Ein Pageview, der keinen Cache-Status meldet, wird leer gespeichert und erscheint im Dashboard als Unknown (in den Share-Metriken pro Status zählt er als unreported). Zusammen sind sie der schnellste Blick darauf, ob dein Edge-Cache wirklich greift, direkt neben TTFB.
Cache-Alter
Der Status sagt, ob der Besucher ein Cache-Objekt bekommen hat, das Alter sagt wie alt es war. Das ist die andere Hälfte der Frage: ein Shop mit 95 % Trefferquote, dessen Objekte im Schnitt vier Stunden alt sind, liefert schnell und veraltet aus.
Server-Timing: fm-cdn-cache;desc="HIT", fm-cdn-age;desc=42, fm-origin-age;desc=312| Key | Spalte | Bedeutung |
|---|---|---|
fm-cdn-age | cdn_age | Alter des ausgelieferten CDN-Objekts, in Sekunden |
fm-origin-age | origin_age | Alter des ausgelieferten Origin-Objekts, in Sekunden |
Regeln für den Wert:
- Nur First-Party-Keys, keine Hersteller-Aliase. Ein bloßes
agesagt nicht, welche Ebene es gemessen hat, und ohne das Präfix hätten wir die Einheit nicht in der Hand: ein Framework, dascache-age;dur=500sendet, meint Millisekunden, und der Wert würde als 500 Sekunden gespeichert. - Sekunden, in
desc, nicht indur.durist als Dauer in Millisekunden spezifiziert, und die DevTools zeichnen es als Balken in der Timing-Ansicht des Dokument-Requests: ein Alter dort würde als Backend-Phase erscheinen, die es nie gegeben hat. Diedur-Schreibweise wird trotzdem akzeptiert, weil eine Site sie zuerst probiert. Sende die nackte Zahl:fm-origin-age;desc=312wird gelesen,desc=312sverworfen. - Sende den Key nur auf einem Cache-Treffer. Auf einem Miss ist das Alter 0 und zieht jedes Perzentil nach unten.
- Werte über einem Jahr werden verworfen und nicht gekappt, denn eine so große Zahl ist ein Timestamp oder ein als Sekunden gesendeter Millisekundenwert, kein Alter.
desc=0ist dagegen ein gültiger Wert: ein Objekt, das gerade eben gespeichert wurde.
Zwei Spalten, nicht eine. Ein Shop hinter einem CDN hat beide Alter im selben Pageview, und sie bedeuten nicht dasselbe. Der Header des Origins friert im CDN-Objekt ein: auf einem CDN-Treffer ist der Origin-Wert das Alter zum Zeitpunkt, an dem das CDN geholt hat, und erst die Summe ist das Alter, das der Besucher gesehen hat. Das Server-Timing-Panel weist deshalb drei Zeilen aus, cdn, origin und content (die Summe auf einem CDN-Treffer), alle in Sekunden und serverseitig berechnet, denn das p75 einer Summe ist nicht die Summe der p75.
Beide Alter stehen als Metriken zur Verfügung (cdn_age, origin_age, mit den üblichen avg und p50 bis p99), als Felder auf den Beacon-Zeilen und im Pageview-Detail neben dem Cache-Status, den sie qualifizieren, sowie über get_server_timing des MCP-Servers.
Seitentyp
Eine reservierte Metrik kennzeichnet die Art der Seite und wird zu einer Dashboard-Dimension, nach der du gruppieren und filtern kannst. Sende das Label im desc (der Dauer-Slot ist numerisch):
Server-Timing: pageType;desc="checkout"Fastmon akzeptiert pageType / page_type und das First-Party fm-pagetype, schreibt das Label klein und kappt es bei 32 Zeichen. Der Header-Wert gewinnt immer gegen die Body-Class-Erkennung, die du mit pagetype_ruleset aktivieren kannst, und ist damit die maßgebliche Quelle, sobald du ihn serverseitig setzen kannst.
Origin-Host und Logged-in
Zwei weitere reservierte Metriken tragen ein Label statt einer Dauer, und beide werden zu Dimensionen, nach denen du filtern und gruppieren kannst. Anders als jede andere hochgestufte Spalte haben sie keine Vendor-Aliase: host, node, server oder user aus einem unbekannten Framework könnten alles bedeuten, deine Site steigt also ein, indem sie den eindeutigen First-Party-Namen sendet.
Server-Timing: fm-host;desc="web-03", fm-loggedin;desc="no"fm-host → origin_host benennt die Maschine deines Origin-Tiers, die genau diesen Pageview gerendert hat. Der Wert der Aufteilung zeigt sich im Cluster: Läuft ein Knoten von fünf heiß (schlechter Deploy, volle Platte, lauter Nachbar), zeigt das aggregierte backend_dur nur ein verschobenes p95, die Aufteilung pro Knoten dagegen die Kiste, in die du schauen musst. Regeln für den Wert:
- Sende den kurzen, stabilen Hostnamen, nicht den FQDN und keine URL. In Kubernetes nimm einen stabilen Namen (StatefulSet-Ordinal, Node-Name, Deployment-Slot), nie den Pod-Namen mit Zufalls-Suffix: Der wechselt bei jedem Deploy und füllt die Dimension mit Werten, die schon tot sind, wenn du sie liest.
- Ein Wert über 32 Zeichen wird verworfen, nicht gekürzt:
gethostname()liefert auf AWS und in k8s den vollen internen FQDN, und ein abgeschnittener Hostname wäre ein falscher Knotenname (zwei Maschinen, deren Namen sich erst nach Zeichen 32 unterscheiden, würden zu einem Eintrag verschmelzen). Bleibt die Spalte leer, ist dein Wert zu lang.
fm-loggedin → logged_in sagt, ob die Seite für einen eingeloggten Besucher gerendert wurde. Das ist die Trennlinie, die Cache-Verhalten erklärt: Ein personalisierter Request umgeht den Full-Page-Cache per Definition, meldet also MISS/PASS und ein Vielfaches der anonymen Backend-Zeit. Ohne das Flag landen beide Gruppen in einem Mittelwert und jede Cache-Hit-Rate ist still verfälscht; mit ihm löst sich „unser TTFB ist schlechter geworden" in „der eingeloggte Anteil ist gewachsen" auf.
yes / no reicht; true/false, 1/0, y/n, in/out und anonymous werden darauf normalisiert. Ein eigener dritter Zustand (etwa b2b) wird durchgereicht, wenn er Slug-Form hat (^[a-z][a-z0-9_-]{0,15}$); alles andere wird verworfen statt geraten.
Nur das grobe Flag
fm-loggedin darf den Render-Modus tragen und sonst nichts: keine Kundennummer, keine Kundengruppen-ID, keine Rolle, kein Benutzername. fm-host beschreibt deinen Server, nie den Besucher; Request- und Session-Kennungen gehören nicht hinein. Der Collector erzwingt die Form (Längen-Caps, ID-Heuristik); die Identifier draußen zu halten ist dein Teil des Vertrags.
Pageviews, die keinen der beiden Keys melden, erscheinen als Not reported; der Leerstring ist ein echter Filterwert für genau diese Gruppe, und logged_in ≠ yes ist die Abfrage „nur anonymer, cachebarer Traffic".
Den Header senden
Setze den Header auf die Response deines HTML-Dokuments. Das Format ist eine kommagetrennte Liste von Metriken, jeweils name, optional gefolgt von ;dur=<ms> und/oder ;desc="<text>".
@app.after_request
def add_server_timing(resp):
resp.headers["Server-Timing"] = (
f"db;dur={db_ms:.1f}, "
f"render;dur={render_ms:.1f}, "
f'cdn-cache;desc="{cache_status}", '
f'pageType;desc="{page_type}"'
)
return respres.setHeader('Server-Timing', [
`db;dur=${dbMs.toFixed(1)}`,
`http;dur=${apiMs.toFixed(1)}`,
`render;dur=${renderMs.toFixed(1)}`,
].join(', '));// src/EventListener/ServerTimingListener.php
#[AsEventListener(event: ResponseEvent::class)]
final class ServerTimingListener
{
public function __invoke(ResponseEvent $event): void
{
$event->getResponse()->headers->set('Server-Timing', sprintf(
'db;dur=%.1f, render;dur=%.1f, cdn-cache;desc="%s"',
$dbMs,
$renderMs,
$cacheStatus,
));
}
}Die meisten CDNs (Cloudflare, Fastly, CloudFront) können ihre eigene Cache-/Edge-Metrik an der Edge an denselben Header anhängen, sodass der Browser sowohl die Phasen deines Origins als auch das Cache-Urteil der Edge an einer Stelle sieht. Und fastmon kennt deren native Namen bereits (cfcachestatus, cfedge, die CloudFront-Flags, …).
Verifizieren
- Öffne deine Site, dann DevTools → Network → Dokument-Request anklicken → Headers. Du solltest deinen
Server-Timing-Header auf der Response sehen. - Im Timing-Tab desselben Requests rendert Chrome die
Server-Timing-Einträge inline (ein schneller Sanity-Check, dass die Syntax geparst wurde). - Öffne in fastmon Analytics → Server-Timing. Neue Daten erscheinen mit den nächsten Beacons; gib den Daten ein paar Pageviews Zeit, bevor du Perzentile liest.
Häufige Überraschungen
- Nur die Dokument-Response zählt. Fastmon liest den Navigation-Entry, nicht das
Server-Timingdeiner Sub-Ressourcen (Skripte, Bilder, API-Calls). Für In-Page-Request-Timing siehe stattdessen Fetch/XHR. durist optional. Eine Metrik mit nur einemdesc(wie ein Cache-HIT/MISSoder ein Seitentyp) ist gültig und nützlich. Genau so werden das Cache-Status- und das Seitentyp-Feld gespeist.- Cross-Origin-Dokumente brauchen freigegebenes Timing. Ein Same-Origin-Dokument ist per Default in Ordnung; wird dein HTML von einem anderen Origin ausgeliefert, wird
serverTimingnur befüllt, wenn diese Response auchTiming-Allow-Originsendet. - Keine Internas im
descleaken. Die Description ist Freitext, der zum Browser und wieder zurück übertragen wird. Was wie eine UUID oder ein langer Token aussieht, wird verworfen. Beschränke dich aber ohnehin auf grobe Labels (HIT,MISS, Region-Codes); nie Query-Text, IDs oder Secrets dort ablegen. - Maximal acht Custom-Keys. Über den Katalog hinaus werden nur die ersten acht unbekannten Namen pro Pageview behalten. Wenn du viele Custom-Metriken erfindest, fasse die dauerhaften unter einem Namen zusammen, den fastmon bereits erkennt.
- Serverzeit länger als TTFB ist kein Datenfehler. Hinter 103 Early Hints ist das erste Byte die vorläufige Antwort der Edge, und die Phasen beschreiben die finale;
edge_dur + backend_durmuss inttfb_finalpassen, nicht inttfb. Siehe TTFB → Early Hints.
Verwandt
- TTFB: die Metrik, die Server-Timing erklärt.
- Der RUM-Beacon: jedes Feld, das der Beacon liest, inklusive
Server-Timing. - Analytics: wo der Server-Timing-View lebt.
- Query Server Timing: die API hinter dem Panel (nur auf Englisch).