fastmon Docs
Guides

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.4

Der 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:

FeldQuelleWas es ist
nameder Metriknamedb, cache, app, …: das Label, das du wählst
durdur=die gemeldete Dauer in Millisekunden (optional)
descdesc=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). Ein tideways.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 numerisches desc (z. B. items;desc=3) wird als Dauer interpretiert, aber nur, wenn es in dasselbe Fenster passt; eine Zahl außerhalb (Shopifys theme;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 (Shopifys compressionlevel und speculationtags), 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:

  1. Hochgestufte Skalare (immer behalten): die neun Backend-Phasen, die zwei Cache-Status, den Seitentyp und die zwei Label-Dimensionen (origin_host, logged_in). Sie werden zu dedizierten Spalten.
  2. Tier-2-Katalog (ohne Cap): ein bekannter Satz von Namen (kafka, queue, dns, cpu, auth, gc, parse, session, theme, twig, apcu, compressiontime sowie die Mitglieder der summierten Phasen unten) landet in den Dauer-/Description-Maps des Pageviews.
  3. 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 neun Phasen abgebildet, gezeigt als p50 / p75 / p95 über den Zeitraum, jeweils mit der Zahl der Pageviews, die die Phase gemeldet haben. Fünf 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.

PhaseErkannte Namen (in Vorrang-Reihenfolge)Wie
edge_durfm-edge, cfedge, edge, time-elapsederster mit dur
origin_durfm-origin, cforigin, origin, cdn-upstream-fblerster mit dur
backend_durfm-backend, processing, total, app, be, wp-total, bootstraperster mit dur
db_durfm-db, db, sql, rdbms; dazu mongodb, sqliteerster mit dur, + Summe
render_durfm-render, render, wp-template, view, ssrerster mit dur
http_durfm-http, http, fetch, api, extSumme
search_durfm-search, elasticsearch, opensearch, solr, searchSumme
kv_durfm-kv, redis, valkey, kv, memcache, cacheSumme
code_durfm-code, fm-unknown, unknownerster mit dur

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.

Bei einem CDN-Treffer speichert fastmon keine Origin-Phasen. Ein CDN spielt den Header des Origins aus dem Request ab, der den Cache gefüllt hat. Ist der CDN-Status eines Pageviews HIT, STALE, UPDATING oder REVALIDATED, verwirft fastmon die Dauern von origin, backend, db, http, render, search, kv und code sowie die eigenen Keys mit Dauer. edge_dur, die Cache-Status, Alter, TTLs und Label-Keys bleiben.

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. code_dur ist die Zeit in deinem eigenen Code außerhalb aller anderen Schichten, im Dashboard „Code". Das Shopware-Plugin reicht dafür den Tideways-Layer unknown durch.

Alle neun 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, Render und Code 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:

SchichtErkannte Namen (in Vorrang-Reihenfolge)
cdnfm-cdn-cache, fm-cdn, cfcachestatus, cdn-cache, hit-state
originfm-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.

StatusBedeutung
HITAus dem Cache ausgeliefert, kein Origin-Abruf.
MISSNicht im Cache; vom Origin geholt (und meist für das nächste Mal gespeichert).
EXPIREDEine gecachte Kopie war abgelaufen und wurde vom Origin aufgefrischt.
STALEEine veraltete Kopie wurde ausgeliefert (stale-while-revalidate oder Origin nicht erreichbar).
REVALIDATEDDie gecachte Kopie wurde gegen den Origin geprüft und als noch frisch bestätigt.
UPDATINGEine veraltete Kopie wurde ausgeliefert, während der Cache sie im Hintergrund auffrischte.
BYPASSDer Cache wurde für diese Anfrage bewusst übersprungen; der Origin lieferte aus.
DYNAMICAls dynamisch und nicht cachebar behandelt; immer vom Origin.
PASSUngecacht durchgereicht (Fastly pass; HITPASS kollabiert hierher).
NONEKein 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

Das Alter sagt, wie alt das ausgelieferte Objekt war, in Sekunden. Ein Shop mit 95 % Trefferquote und vier Stunden alten Objekten liefert schnell, aber veraltet aus.

Server-Timing: fm-cdn-cache;desc="HIT", fm-cdn-age;desc=42, fm-origin-age;desc=312
Keys (in Rangfolge)SpalteBedeutung
fm-cdn-age, cdn-cache-agecdn_ageAlter des ausgelieferten CDN-Objekts, in Sekunden
fm-origin-age, origin-cache-ageorigin_ageAlter des ausgelieferten Origin-Objekts, in Sekunden

Regeln für den Wert:

  • Sekunden, in desc. fm-origin-age;desc=312 wird gelesen, desc=312s verworfen. Die fm-*-Keys nehmen auch dur, die Hersteller-Keys verwerfen es.
  • fm-* gewinnt, wenn eine Site ihn neben dem Hersteller-Key sendet. Ein CDN, das cdn-cache-age sendet, füllt das Alter ohne Änderung an deiner Site.
  • Sende den Key nur bei einem Cache-Treffer. Bei einem Miss ist das Alter 0 und zieht jedes Perzentil nach unten.
  • Werte über einem Jahr werden verworfen. desc=0 ist gültig: ein Objekt, das gerade erst gespeichert wurde.

Bei einem CDN-Treffer ist erst die Summe das Alter, das der Besucher gesehen hat. Der Header des Origins friert im CDN-Objekt ein. Das Server-Timing-Panel zeigt cdn, origin und content (die Summe bei einem CDN-Treffer), alle in Sekunden.

Cache-TTL

Die TTL ist die Frische, die dem Objekt bei der Auslieferung noch blieb, in Sekunden. Alter plus TTL ergibt die konfigurierte Lebensdauer.

Keys (in Rangfolge)Spalte
fm-cdn-ttl, cdn-cache-ttlcdn_ttl
fm-origin-ttl, origin-cache-ttlorigin_ttl

Für den Wert gelten die Regeln des Alters. Das Panel zeigt p50, p75 und p95 pro Ebene, ohne content-Zeile. Die Cache-Seite zeigt Alter und TTL als Balken pro Ebene und pro Seitentyp, außerdem im Zeitverlauf: Ein Purge fällt dort als Alter auf, das auf null springt.

Alle vier sind Metriken mit den üblichen avg und p50 bis p99 und Metrik-Filter in Sekunden. Ein Pageview ohne den Key trifft einen solchen Filter nie. Du findest sie außerdem auf den Beacon-Zeilen, im Pageview-Detail, in Benachrichtigungsregeln, in der Besucher-Journey und in 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>".

Flask / Python
@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 resp
Express / Node
res.setHeader('Server-Timing', [
  `db;dur=${dbMs.toFixed(1)}`,
  `http;dur=${apiMs.toFixed(1)}`,
  `render;dur=${renderMs.toFixed(1)}`,
].join(', '));
Symfony / PHP
// 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

  1. Öffne deine Site, dann DevTools → Network → Dokument-Request anklicken → Headers. Du solltest deinen Server-Timing-Header auf der Response sehen.
  2. Im Timing-Tab desselben Requests rendert Chrome die Server-Timing-Einträge inline (ein schneller Sanity-Check, dass die Syntax geparst wurde).
  3. Ö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-Timing deiner Sub-Ressourcen (Skripte, Bilder, API-Calls). Für In-Page-Request-Timing siehe stattdessen Fetch/XHR.
  • dur ist optional. Eine Metrik mit nur einem desc (wie ein Cache-HIT/MISS oder 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 serverTiming nur befüllt, wenn diese Response auch Timing-Allow-Origin sendet.
  • Keine Internas im desc leaken. 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_dur muss in ttfb_final passen, nicht in ttfb. 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).

On this page