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

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

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:

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-hitHIT, cdn-cache-missMISS, cdn-cache-refreshREVALIDATED (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-CLUSTERMISS, HITPASSPASS, HIT-STALESTALE); 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

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
KeySpalteBedeutung
fm-cdn-agecdn_ageAlter des ausgelieferten CDN-Objekts, in Sekunden
fm-origin-ageorigin_ageAlter des ausgelieferten Origin-Objekts, in Sekunden

Regeln für den Wert:

  • Nur First-Party-Keys, keine Hersteller-Aliase. Ein bloßes age sagt nicht, welche Ebene es gemessen hat, und ohne das Präfix hätten wir die Einheit nicht in der Hand: ein Framework, das cache-age;dur=500 sendet, meint Millisekunden, und der Wert würde als 500 Sekunden gespeichert.
  • Sekunden, in desc, nicht in dur. dur ist 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. Die dur-Schreibweise wird trotzdem akzeptiert, weil eine Site sie zuerst probiert. Sende die nackte Zahl: fm-origin-age;desc=312 wird gelesen, desc=312s verworfen.
  • 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=0 ist 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-hostorigin_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-loggedinlogged_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