[{"data":1,"prerenderedAt":1400},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers":1391},{"id":4,"title":5,"body":6,"description":1371,"extension":1372,"meta":1373,"navigation":1385,"path":1386,"seo":1387,"stem":1389,"__hash__":1390},"content\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers\u002Findex.md","Exposing Backend Timing with Server-Timing Headers",{"type":7,"value":8,"toc":1356},"minimark",[9,14,29,45,202,207,240,244,250,256,262,268,346,350,355,365,710,720,724,727,925,928,932,1078,1081,1085,1098,1101,1208,1212,1222,1226,1233,1237,1259,1263,1275,1284,1295,1314,1318,1341,1346,1349,1352],[10,11,13],"h1",{"id":12},"how-to-expose-backend-timing-with-server-timing-headers","How to Expose Backend Timing with Server-Timing Headers",[15,16,17,18,23,24,28],"p",{},"This guide adds the server's side of the story to ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002F","RUM Beacons & Field Data Collection",", within ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",". Time to First Byte is the first phase of every LCP, and in RUM it is a single opaque number: the browser knows when it sent the request and when the first byte arrived, nothing about what happened in between. Was it a CDN cache miss? A slow database query? Server-side rendering of a heavy component tree? A cold serverless start?",[15,30,31,32,36,37,40,41,44],{},"The ",[33,34,35],"code",{},"Server-Timing"," response header answers those questions in the field. The server lists named metrics with durations — ",[33,38,39],{},"cdn-cache;desc=MISS, db;dur=184, render;dur=96"," — and the browser exposes them to JavaScript through ",[33,42,43],{},"PerformanceResourceTiming.serverTiming"," and on the navigation entry. Your RUM beacon reads them alongside LCP, and a slow TTFB becomes \"slow because the product query took 400ms on cache misses for this template\".",[15,46,47],{},[48,49,55,56,55,63,55,67,55,70,55,88,55,96,55,102,55,109,55,116,55,119,55,123,55,126,55,130,55,133,55,137,55,146,55,149,55,152,55,155,55,161,55,167,55,171,55,176,55,181,55,185,55,190,55,194,55,198,55],"svg",{"viewBox":50,"width":51,"role":52,"ariaLabel":53,"style":54},"0 0 760 330","100%","img","Sequence showing the origin adding Server-Timing entries, the CDN appending its cache status, and the browser exposing both to the RUM script.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[57,58],"rect",{"className":59,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],"svg-canvas","0","#ffffff",[64,65,66],"title",{},"Server-Timing from origin to RUM",[68,69,53],"desc",{},[71,72,73],"defs",{},[74,75,82],"marker",{"id":76,"viewBox":77,"refX":78,"refY":79,"markerWidth":80,"markerHeight":80,"orient":81},"faaca08841","0 0 10 10","9","5","7","auto-start-reverse",[83,84],"path",{"d":85,"fill":86,"style":87},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[57,89],{"x":90,"y":90,"width":91,"height":92,"rx":93,"fill":94,"stroke":86,"style":95},"1","758","328","10","none","stroke-opacity:0.18",[97,98,66],"text",{"x":99,"y":100,"fill":86,"style":101},"28.0","34.0","font-size:16px;font-weight:700",[57,103],{"x":104,"y":105,"width":106,"height":100,"rx":107,"fill":86,"stroke":86,"style":108},"41.0","56.0","150.0","6","fill-opacity:0.06;stroke-opacity:0.4",[97,110,115],{"x":111,"y":112,"fill":86,"style":113,"textAnchor":114},"116.0","78.0","font-size:13px;font-weight:700","middle","RUM script",[57,117],{"x":118,"y":105,"width":106,"height":100,"rx":107,"fill":86,"stroke":86,"style":108},"217.0",[97,120,122],{"x":121,"y":112,"fill":86,"style":113,"textAnchor":114},"292.0","Browser",[57,124],{"x":125,"y":105,"width":106,"height":100,"rx":107,"fill":86,"stroke":86,"style":108},"393.0",[97,127,129],{"x":128,"y":112,"fill":86,"style":113,"textAnchor":114},"468.0","CDN",[57,131],{"x":132,"y":105,"width":106,"height":100,"rx":107,"fill":86,"stroke":86,"style":108},"569.0",[97,134,136],{"x":135,"y":112,"fill":86,"style":113,"textAnchor":114},"644.0","Origin",[138,139],"line",{"x1":111,"y1":140,"x2":111,"y2":141,"stroke":86,"strokeWidth":142,"strokeDashArray":143,"style":145},"90.0","308.0","1.5",[144,144],"4","stroke-opacity:0.35",[138,147],{"x1":121,"y1":140,"x2":121,"y2":141,"stroke":86,"strokeWidth":142,"strokeDashArray":148,"style":145},[144,144],[138,150],{"x1":128,"y1":140,"x2":128,"y2":141,"stroke":86,"strokeWidth":142,"strokeDashArray":151,"style":145},[144,144],[138,153],{"x1":135,"y1":140,"x2":135,"y2":141,"stroke":86,"strokeWidth":142,"strokeDashArray":154,"style":145},[144,144],[138,156],{"x1":121,"y1":157,"x2":158,"y2":157,"stroke":86,"strokeWidth":142,"style":159,"markerEnd":160},"124.0","466.0","stroke-opacity:0.6","url(#faaca08841)",[97,162,166],{"x":163,"y":164,"fill":86,"style":165,"textAnchor":114},"380.0","117.0","font-size:12px","GET \u002Fproduct\u002F42",[138,168],{"x1":128,"y1":169,"x2":170,"y2":169,"stroke":86,"strokeWidth":142,"style":159,"markerEnd":160},"164.0","642.0",[97,172,175],{"x":173,"y":174,"fill":86,"style":165,"textAnchor":114},"556.0","157.0","cache MISS",[138,177],{"x1":135,"y1":178,"x2":179,"y2":178,"stroke":86,"strokeWidth":142,"strokeDashArray":180,"style":159,"markerEnd":160},"204.0","470.0",[79,144],[97,182,184],{"x":173,"y":183,"fill":86,"style":165,"textAnchor":114},"197.0","db;dur=184, render;dur=96",[138,186],{"x1":128,"y1":187,"x2":188,"y2":187,"stroke":86,"strokeWidth":142,"strokeDashArray":189,"style":159,"markerEnd":160},"244.0","294.0",[79,144],[97,191,193],{"x":163,"y":192,"fill":86,"style":165,"textAnchor":114},"237.0","+ cdn-cache;desc=MISS",[138,195],{"x1":111,"y1":196,"x2":197,"y2":196,"stroke":86,"strokeWidth":142,"style":159,"markerEnd":160},"284.0","290.0",[97,199,201],{"x":178,"y":200,"fill":86,"style":165,"textAnchor":114},"277.0","read serverTiming",[203,204,206],"h2",{"id":205},"rapid-diagnosis","Rapid Diagnosis",[208,209,210,218,224,230],"ul",{},[211,212,213,217],"li",{},[214,215,216],"strong",{},"Check for the header today."," In DevTools Network, select the document request and open the Timing tab; any Server-Timing entries appear at the bottom. Many CDNs and frameworks add some already.",[211,219,220,223],{},[214,221,222],{},"Correlate TTFB with LCP."," In RUM, if TTFB p75 is a large fraction of LCP p75 (say over 30%), the backend deserves attribution.",[211,225,226,229],{},[214,227,228],{},"Check variance."," A bimodal TTFB distribution (fast cluster plus slow cluster) usually means cache hits vs misses — exactly what a cache-status entry reveals.",[211,231,232,235,236,239],{},[214,233,234],{},"Check cross-origin access."," Server-Timing on cross-origin resources is only exposed if the response includes ",[33,237,238],{},"Timing-Allow-Origin",".",[203,241,243],{"id":242},"root-cause-analysis","Root Cause Analysis",[15,245,246,249],{},[214,247,248],{},"1. TTFB is a black box."," Without server attribution, field TTFB can only be compared over time, not explained.",[15,251,252,255],{},[214,253,254],{},"2. Server logs are disconnected from user experience."," Backend dashboards show query times per request; RUM shows TTFB per page view; joining them requires a shared identifier that is rarely in place.",[15,257,258,261],{},[214,259,260],{},"3. Cache behaviour is invisible."," Edge hit ratio dashboards report aggregates; they do not tell you which users and pages experienced misses and how slow those were.",[15,263,264,267],{},[214,265,266],{},"4. Cold starts and queueing."," Serverless cold starts and request queueing add latency that never appears in application timers measured inside the handler.",[15,269,270],{},[48,271,55,274,55,277,55,280,55,282,55,285,55,287,55,294,55,302,55,307,55,311,55,315,55,319,55,323,55,330,55,334,55,338,55,342,55],{"viewBox":272,"width":51,"role":52,"ariaLabel":273,"style":54},"0 0 760 194","Bar chart showing p75 TTFB for cache hits, stale responses, misses and cold-start misses on a product template.",[57,275],{"className":276,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],[64,278,279],{},"Field TTFB p75 split by Server-Timing cache status",[68,281,273],{},[57,283],{"x":90,"y":90,"width":91,"height":284,"rx":93,"fill":94,"stroke":86,"style":95},"192",[97,286,279],{"x":99,"y":100,"fill":86,"style":101},[97,288,293],{"x":289,"y":290,"fill":86,"style":291,"textAnchor":292},"151.9","70.0","font-size:13px","end","CDN HIT",[57,295],{"x":296,"y":105,"width":297,"height":298,"rx":299,"fill":300,"stroke":300,"style":301},"163.9","32.4","19","3","#0466c8","fill-opacity:0.55;stroke-opacity:0.9",[97,303,306],{"x":304,"y":290,"fill":86,"style":305},"202.2","font-size:12px;font-weight:600","95ms",[97,308,310],{"x":289,"y":309,"fill":86,"style":291,"textAnchor":292},"101.0","STALE (revalidating)",[57,312],{"x":296,"y":313,"width":314,"height":298,"rx":299,"fill":300,"stroke":300,"style":301},"87.0","37.5",[97,316,318],{"x":317,"y":309,"fill":86,"style":305},"207.3","110ms",[97,320,322],{"x":289,"y":321,"fill":86,"style":291,"textAnchor":292},"132.0","MISS",[57,324],{"x":296,"y":325,"width":326,"height":298,"rx":299,"fill":327,"stroke":328,"style":329},"118.0","235.0","#ffc300","#b8860b","fill-opacity:0.7;stroke-opacity:0.9",[97,331,333],{"x":332,"y":321,"fill":86,"style":305},"404.9","690ms",[97,335,337],{"x":289,"y":336,"fill":86,"style":291,"textAnchor":292},"163.0","MISS + cold start",[57,339],{"x":296,"y":340,"width":341,"height":298,"rx":299,"fill":327,"stroke":328,"style":329},"149.0","504.1",[97,343,345],{"x":344,"y":336,"fill":86,"style":305},"674.0","1480ms",[203,347,349],{"id":348},"step-by-step-resolution","Step-by-Step Resolution",[351,352,354],"h3",{"id":353},"_1-emit-server-timing-from-the-origin","1. Emit Server-Timing from the origin",[15,356,357,358,361,362,364],{},"Time the phases you control and add them as entries. Keep names short and stable; ",[33,359,360],{},"dur"," is in milliseconds, ",[33,363,68],{}," is free text.",[366,367,372],"pre",{"className":368,"code":369,"language":370,"meta":371,"style":371},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Express\u002FNode middleware — append phase timings to the response.\napp.use((req, res, next) => {\n  const marks = [];\n  res.locals.time = async (name, fn) => {\n    const t0 = performance.now();\n    try { return await fn(); } finally { marks.push(`${name};dur=${(performance.now() - t0).toFixed(1)}`); }\n  };\n  const writeHead = res.writeHead;\n  res.writeHead = function (...args) {\n    if (marks.length) res.setHeader('Server-Timing', marks.join(', '));\n    return writeHead.apply(this, args);\n  };\n  next();\n});\n\u002F\u002F in a handler: const product = await res.locals.time('db', () => getProduct(id));\n\u002F\u002F trade-off: Server-Timing is visible to anyone who opens DevTools. Do not\n\u002F\u002F include internal hostnames, query text or anything sensitive in desc values;\n\u002F\u002F strip or gate detailed entries for unauthenticated traffic if necessary.\n","javascript","",[33,373,374,382,421,437,468,488,566,572,585,610,647,667,672,680,686,692,698,704],{"__ignoreMap":371},[375,376,378],"span",{"class":138,"line":377},1,[375,379,381],{"class":380},"sjfSM","\u002F\u002F Express\u002FNode middleware — append phase timings to the response.\n",[375,383,385,389,393,396,400,403,406,408,411,414,418],{"class":138,"line":384},2,[375,386,388],{"class":387},"saISM","app.",[375,390,392],{"class":391},"smZ65","use",[375,394,395],{"class":387},"((",[375,397,399],{"class":398},"sQw3B","req",[375,401,402],{"class":387},", ",[375,404,405],{"class":398},"res",[375,407,402],{"class":387},[375,409,410],{"class":398},"next",[375,412,413],{"class":387},") ",[375,415,417],{"class":416},"sPARh","=>",[375,419,420],{"class":387}," {\n",[375,422,424,427,431,434],{"class":138,"line":423},3,[375,425,426],{"class":416},"  const",[375,428,430],{"class":429},"sPXB4"," marks",[375,432,433],{"class":416}," =",[375,435,436],{"class":387}," [];\n",[375,438,440,443,446,448,451,454,457,459,462,464,466],{"class":138,"line":439},4,[375,441,442],{"class":387},"  res.locals.",[375,444,445],{"class":391},"time",[375,447,433],{"class":416},[375,449,450],{"class":416}," async",[375,452,453],{"class":387}," (",[375,455,456],{"class":398},"name",[375,458,402],{"class":387},[375,460,461],{"class":398},"fn",[375,463,413],{"class":387},[375,465,417],{"class":416},[375,467,420],{"class":387},[375,469,471,474,477,479,482,485],{"class":138,"line":470},5,[375,472,473],{"class":416},"    const",[375,475,476],{"class":429}," t0",[375,478,433],{"class":416},[375,480,481],{"class":387}," performance.",[375,483,484],{"class":391},"now",[375,486,487],{"class":387},"();\n",[375,489,491,494,497,500,503,506,509,512,515,518,521,525,527,530,532,535,537,539,542,545,547,550,553,555,557,560,563],{"class":138,"line":490},6,[375,492,493],{"class":416},"    try",[375,495,496],{"class":387}," { ",[375,498,499],{"class":416},"return",[375,501,502],{"class":416}," await",[375,504,505],{"class":391}," fn",[375,507,508],{"class":387},"(); } ",[375,510,511],{"class":416},"finally",[375,513,514],{"class":387}," { marks.",[375,516,517],{"class":391},"push",[375,519,520],{"class":387},"(",[375,522,524],{"class":523},"sZ8jY","`${",[375,526,456],{"class":387},[375,528,529],{"class":523},"};dur=${",[375,531,520],{"class":523},[375,533,534],{"class":387},"performance",[375,536,239],{"class":523},[375,538,484],{"class":391},[375,540,541],{"class":523},"() ",[375,543,544],{"class":416},"-",[375,546,476],{"class":387},[375,548,549],{"class":523},").",[375,551,552],{"class":391},"toFixed",[375,554,520],{"class":523},[375,556,90],{"class":429},[375,558,559],{"class":523},")",[375,561,562],{"class":523},"}`",[375,564,565],{"class":387},"); }\n",[375,567,569],{"class":138,"line":568},7,[375,570,571],{"class":387},"  };\n",[375,573,575,577,580,582],{"class":138,"line":574},8,[375,576,426],{"class":416},[375,578,579],{"class":429}," writeHead",[375,581,433],{"class":416},[375,583,584],{"class":387}," res.writeHead;\n",[375,586,588,591,594,596,599,601,604,607],{"class":138,"line":587},9,[375,589,590],{"class":387},"  res.",[375,592,593],{"class":391},"writeHead",[375,595,433],{"class":416},[375,597,598],{"class":416}," function",[375,600,453],{"class":387},[375,602,603],{"class":416},"...",[375,605,606],{"class":398},"args",[375,608,609],{"class":387},") {\n",[375,611,613,616,619,622,625,628,630,633,636,639,641,644],{"class":138,"line":612},10,[375,614,615],{"class":416},"    if",[375,617,618],{"class":387}," (marks.",[375,620,621],{"class":429},"length",[375,623,624],{"class":387},") res.",[375,626,627],{"class":391},"setHeader",[375,629,520],{"class":387},[375,631,632],{"class":523},"'Server-Timing'",[375,634,635],{"class":387},", marks.",[375,637,638],{"class":391},"join",[375,640,520],{"class":387},[375,642,643],{"class":523},"', '",[375,645,646],{"class":387},"));\n",[375,648,650,653,656,659,661,664],{"class":138,"line":649},11,[375,651,652],{"class":416},"    return",[375,654,655],{"class":387}," writeHead.",[375,657,658],{"class":391},"apply",[375,660,520],{"class":387},[375,662,663],{"class":429},"this",[375,665,666],{"class":387},", args);\n",[375,668,670],{"class":138,"line":669},12,[375,671,571],{"class":387},[375,673,675,678],{"class":138,"line":674},13,[375,676,677],{"class":391},"  next",[375,679,487],{"class":387},[375,681,683],{"class":138,"line":682},14,[375,684,685],{"class":387},"});\n",[375,687,689],{"class":138,"line":688},15,[375,690,691],{"class":380},"\u002F\u002F in a handler: const product = await res.locals.time('db', () => getProduct(id));\n",[375,693,695],{"class":138,"line":694},16,[375,696,697],{"class":380},"\u002F\u002F trade-off: Server-Timing is visible to anyone who opens DevTools. Do not\n",[375,699,701],{"class":138,"line":700},17,[375,702,703],{"class":380},"\u002F\u002F include internal hostnames, query text or anything sensitive in desc values;\n",[375,705,707],{"class":138,"line":706},18,[375,708,709],{"class":380},"\u002F\u002F strip or gate detailed entries for unauthenticated traffic if necessary.\n",[15,711,712,713,402,716,719],{},"Expected outcome: document responses carry ",[33,714,715],{},"db",[33,717,718],{},"render"," and other named durations.",[351,721,723],{"id":722},"_2-add-edge-timing-and-cache-status","2. Add edge timing and cache status",[15,725,726],{},"Most CDNs can append their own entries (cache status, edge processing time) via configuration or edge code, so you see where in the chain time was spent.",[366,728,730],{"className":368,"code":729,"language":370,"meta":371,"style":371},"\u002F\u002F Cloudflare Worker: forward origin timing and add cache status.\nexport default {\n  async fetch(req, env, ctx) {\n    const t0 = Date.now();\n    const res = await fetch(req, { cf: { cacheEverything: true } });\n    const out = new Response(res.body, res);\n    const status = res.headers.get('cf-cache-status') || 'NONE';\n    out.headers.append('Server-Timing', `edge;dur=${Date.now() - t0}, cdn-cache;desc=${status}`);\n    return out;\n  },\n};\n\u002F\u002F trade-off: Date.now() inside a Worker is coarsened and does not advance\n\u002F\u002F during CPU work, so edge durations reflect I\u002FO wait. That is the part you\n\u002F\u002F usually care about, but it is not a full CPU profile.\n",[33,731,732,737,747,761,776,798,816,847,888,895,903,910,915,920],{"__ignoreMap":371},[375,733,734],{"class":138,"line":377},[375,735,736],{"class":380},"\u002F\u002F Cloudflare Worker: forward origin timing and add cache status.\n",[375,738,739,742,745],{"class":138,"line":384},[375,740,741],{"class":416},"export",[375,743,744],{"class":416}," default",[375,746,420],{"class":398},[375,748,749,752,755,758],{"class":138,"line":423},[375,750,751],{"class":416},"  async",[375,753,754],{"class":391}," fetch",[375,756,757],{"class":398},"(req, env, ctx) ",[375,759,760],{"class":387},"{\n",[375,762,763,765,767,769,772,774],{"class":138,"line":439},[375,764,473],{"class":416},[375,766,476],{"class":429},[375,768,433],{"class":416},[375,770,771],{"class":387}," Date.",[375,773,484],{"class":391},[375,775,487],{"class":387},[375,777,778,780,783,785,787,789,792,795],{"class":138,"line":470},[375,779,473],{"class":416},[375,781,782],{"class":429}," res",[375,784,433],{"class":416},[375,786,502],{"class":416},[375,788,754],{"class":391},[375,790,791],{"class":387},"(req, { cf: { cacheEverything: ",[375,793,794],{"class":429},"true",[375,796,797],{"class":387}," } });\n",[375,799,800,802,805,807,810,813],{"class":138,"line":490},[375,801,473],{"class":416},[375,803,804],{"class":429}," out",[375,806,433],{"class":416},[375,808,809],{"class":416}," new",[375,811,812],{"class":391}," Response",[375,814,815],{"class":387},"(res.body, res);\n",[375,817,818,820,823,825,828,831,833,836,838,841,844],{"class":138,"line":568},[375,819,473],{"class":416},[375,821,822],{"class":429}," status",[375,824,433],{"class":416},[375,826,827],{"class":387}," res.headers.",[375,829,830],{"class":391},"get",[375,832,520],{"class":387},[375,834,835],{"class":523},"'cf-cache-status'",[375,837,413],{"class":387},[375,839,840],{"class":416},"||",[375,842,843],{"class":523}," 'NONE'",[375,845,846],{"class":387},";\n",[375,848,849,852,855,857,859,861,864,867,869,871,873,875,877,880,883,885],{"class":138,"line":574},[375,850,851],{"class":387},"    out.headers.",[375,853,854],{"class":391},"append",[375,856,520],{"class":387},[375,858,632],{"class":523},[375,860,402],{"class":387},[375,862,863],{"class":523},"`edge;dur=${",[375,865,866],{"class":387},"Date",[375,868,239],{"class":523},[375,870,484],{"class":391},[375,872,541],{"class":523},[375,874,544],{"class":416},[375,876,476],{"class":387},[375,878,879],{"class":523},"}, cdn-cache;desc=${",[375,881,882],{"class":387},"status",[375,884,562],{"class":523},[375,886,887],{"class":387},");\n",[375,889,890,892],{"class":138,"line":587},[375,891,652],{"class":416},[375,893,894],{"class":387}," out;\n",[375,896,897,900],{"class":138,"line":612},[375,898,899],{"class":387},"  }",[375,901,902],{"class":398},",\n",[375,904,905,908],{"class":138,"line":649},[375,906,907],{"class":398},"}",[375,909,846],{"class":387},[375,911,912],{"class":138,"line":669},[375,913,914],{"class":380},"\u002F\u002F trade-off: Date.now() inside a Worker is coarsened and does not advance\n",[375,916,917],{"class":138,"line":674},[375,918,919],{"class":380},"\u002F\u002F during CPU work, so edge durations reflect I\u002FO wait. That is the part you\n",[375,921,922],{"class":138,"line":682},[375,923,924],{"class":380},"\u002F\u002F usually care about, but it is not a full CPU profile.\n",[15,926,927],{},"Expected outcome: every document response says whether it was a cache hit and how long the edge spent.",[351,929,931],{"id":930},"_3-collect-entries-in-rum","3. Collect entries in RUM",[366,933,935],{"className":368,"code":934,"language":370,"meta":371,"style":371},"function serverTimings() {\n  const nav = performance.getEntriesByType('navigation')[0];\n  return Object.fromEntries((nav?.serverTiming || []).map((t) => [t.name, t.duration || t.description]));\n}\nonLCP((m) => send({ lcp: m.value, ttfb: performance.getEntriesByType('navigation')[0].responseStart,\n                    server: serverTimings() }));\n\u002F\u002F trade-off: only entries on the navigation response are read here. For API\n\u002F\u002F calls that gate client rendering, also read serverTiming from their resource\n\u002F\u002F entries (requires Timing-Allow-Origin if cross-origin).\n",[33,936,937,948,975,1014,1019,1052,1063,1068,1073],{"__ignoreMap":371},[375,938,939,942,945],{"class":138,"line":377},[375,940,941],{"class":416},"function",[375,943,944],{"class":391}," serverTimings",[375,946,947],{"class":387},"() {\n",[375,949,950,952,955,957,959,962,964,967,970,972],{"class":138,"line":384},[375,951,426],{"class":416},[375,953,954],{"class":429}," nav",[375,956,433],{"class":416},[375,958,481],{"class":387},[375,960,961],{"class":391},"getEntriesByType",[375,963,520],{"class":387},[375,965,966],{"class":523},"'navigation'",[375,968,969],{"class":387},")[",[375,971,61],{"class":429},[375,973,974],{"class":387},"];\n",[375,976,977,980,983,986,989,991,994,997,999,1002,1004,1006,1009,1011],{"class":138,"line":423},[375,978,979],{"class":416},"  return",[375,981,982],{"class":387}," Object.",[375,984,985],{"class":391},"fromEntries",[375,987,988],{"class":387},"((nav?.serverTiming ",[375,990,840],{"class":416},[375,992,993],{"class":387}," []).",[375,995,996],{"class":391},"map",[375,998,395],{"class":387},[375,1000,1001],{"class":398},"t",[375,1003,413],{"class":387},[375,1005,417],{"class":416},[375,1007,1008],{"class":387}," [t.name, t.duration ",[375,1010,840],{"class":416},[375,1012,1013],{"class":387}," t.description]));\n",[375,1015,1016],{"class":138,"line":439},[375,1017,1018],{"class":387},"}\n",[375,1020,1021,1024,1026,1029,1031,1033,1036,1039,1041,1043,1045,1047,1049],{"class":138,"line":470},[375,1022,1023],{"class":391},"onLCP",[375,1025,395],{"class":387},[375,1027,1028],{"class":398},"m",[375,1030,413],{"class":387},[375,1032,417],{"class":416},[375,1034,1035],{"class":391}," send",[375,1037,1038],{"class":387},"({ lcp: m.value, ttfb: performance.",[375,1040,961],{"class":391},[375,1042,520],{"class":387},[375,1044,966],{"class":523},[375,1046,969],{"class":387},[375,1048,61],{"class":429},[375,1050,1051],{"class":387},"].responseStart,\n",[375,1053,1054,1057,1060],{"class":138,"line":490},[375,1055,1056],{"class":387},"                    server: ",[375,1058,1059],{"class":391},"serverTimings",[375,1061,1062],{"class":387},"() }));\n",[375,1064,1065],{"class":138,"line":568},[375,1066,1067],{"class":380},"\u002F\u002F trade-off: only entries on the navigation response are read here. For API\n",[375,1069,1070],{"class":138,"line":574},[375,1071,1072],{"class":380},"\u002F\u002F calls that gate client rendering, also read serverTiming from their resource\n",[375,1074,1075],{"class":138,"line":587},[375,1076,1077],{"class":380},"\u002F\u002F entries (requires Timing-Allow-Origin if cross-origin).\n",[15,1079,1080],{},"Expected outcome: each LCP beacon carries the server phases that produced its TTFB.",[351,1082,1084],{"id":1083},"_4-analyse-ttfb-by-server-phase-and-cache-status","4. Analyse TTFB by server phase and cache status",[15,1086,1087,1088,1091,1092,1094,1095,1097],{},"Group field TTFB by ",[33,1089,1090],{},"cdn-cache"," status and template, and plot ",[33,1093,715],{}," and ",[33,1096,718],{}," durations at p75 for misses. That tells you whether to raise cache hit ratio (longer TTLs, better keys, stale-while-revalidate) or to make misses cheaper (query optimisation, rendering less on the server).",[15,1099,1100],{},"Expected outcome: a ranked list of backend causes of slow TTFB, weighted by how many real page views they affected.",[15,1102,1103],{},[48,1104,55,1107,55,1110,55,1113,55,1115,55,1122,55,1125,55,1127,55,1132,55,1136,55,1140,55,1143,55,1147,55,1150,55,1153,55,1156,55,1159,55,1163,55,1167,55,1171,55,1174,55,1177,55,1180,55,1184,55,1187,55,1190,55,1196,55,1200,55,1204,55],{"viewBox":1105,"width":51,"role":52,"ariaLabel":1106,"style":54},"0 0 760 149","Steps from emitting Server-Timing entries at origin and edge to ranking backend causes of slow TTFB in RUM.",[57,1108],{"className":1109,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],[64,1111,1112],{},"Turning Server-Timing into decisions",[68,1114,1106],{},[71,1116,1117],{},[74,1118,1120],{"id":1119,"viewBox":77,"refX":78,"refY":79,"markerWidth":80,"markerHeight":80,"orient":81},"fa282522cc",[83,1121],{"d":85,"fill":86,"style":87},[57,1123],{"x":90,"y":90,"width":91,"height":1124,"rx":93,"fill":94,"stroke":86,"style":95},"147",[97,1126,1112],{"x":99,"y":100,"fill":86,"style":101},[57,1128],{"x":99,"y":105,"width":1129,"height":1130,"rx":107,"fill":300,"stroke":300,"style":1131},"108.8","71.0","fill-opacity:0.14;stroke-opacity:0.9",[97,1133,1135],{"x":1134,"y":112,"fill":86,"style":113,"textAnchor":114},"82.4","Origin entries",[97,1137,1139],{"x":1134,"y":1138,"fill":86,"style":165,"textAnchor":114},"96.0","db, render, cache",[57,1141],{"x":1142,"y":105,"width":1129,"height":1130,"rx":107,"fill":300,"stroke":300,"style":1131},"176.8",[97,1144,1146],{"x":1145,"y":112,"fill":86,"style":113,"textAnchor":114},"231.2","Edge entries",[97,1148,1149],{"x":1145,"y":1138,"fill":86,"style":165,"textAnchor":114},"cdn-cache status",[57,1151],{"x":1152,"y":105,"width":1129,"height":1130,"rx":107,"fill":300,"stroke":300,"style":1131},"325.6",[97,1154,1155],{"x":163,"y":112,"fill":86,"style":113,"textAnchor":114},"RUM beacon",[97,1157,1158],{"x":163,"y":1138,"fill":86,"style":165,"textAnchor":114},"serverTiming on",[97,1160,1162],{"x":163,"y":1161,"fill":86,"style":165,"textAnchor":114},"112.0","nav entry",[57,1164],{"x":1165,"y":105,"width":1129,"height":1130,"rx":107,"fill":327,"stroke":328,"style":1166},"474.4","fill-opacity:0.24;stroke-opacity:0.9",[97,1168,1170],{"x":1169,"y":112,"fill":86,"style":113,"textAnchor":114},"528.8","Segment TTFB",[97,1172,1173],{"x":1169,"y":1138,"fill":86,"style":165,"textAnchor":114},"by status +",[97,1175,1176],{"x":1169,"y":1161,"fill":86,"style":165,"textAnchor":114},"template",[57,1178],{"x":1179,"y":105,"width":1129,"height":1130,"rx":107,"fill":300,"stroke":300,"style":1131},"623.2",[97,1181,1183],{"x":1182,"y":112,"fill":86,"style":113,"textAnchor":114},"677.6","Fix",[97,1185,1186],{"x":1182,"y":1138,"fill":86,"style":165,"textAnchor":114},"hit ratio or miss",[97,1188,1189],{"x":1182,"y":1161,"fill":86,"style":165,"textAnchor":114},"cost",[138,1191],{"x1":1192,"y1":1193,"x2":1194,"y2":1193,"stroke":86,"strokeWidth":142,"style":159,"markerEnd":1195},"136.8","91.5","174.8","url(#fa282522cc)",[138,1197],{"x1":1198,"y1":1193,"x2":1199,"y2":1193,"stroke":86,"strokeWidth":142,"style":159,"markerEnd":1195},"285.6","323.6",[138,1201],{"x1":1202,"y1":1193,"x2":1203,"y2":1193,"stroke":86,"strokeWidth":142,"style":159,"markerEnd":1195},"434.4","472.4",[138,1205],{"x1":1206,"y1":1193,"x2":1207,"y2":1193,"stroke":86,"strokeWidth":142,"style":159,"markerEnd":1195},"583.2","621.2",[203,1209,1211],{"id":1210},"verification","Verification",[15,1213,1214,1215,1218,1219,1221],{},"Load a page with DevTools open and confirm the Timing tab shows your entries for the document. In RUM, confirm the share of beacons carrying ",[33,1216,1217],{},"server"," fields matches the share of traffic served by instrumented routes. After a backend fix — say, caching a query — the ",[33,1220,715],{}," p75 for misses and the MISS-segment TTFB p75 should both fall, and overall LCP p75 should move by roughly the TTFB change multiplied by the miss rate.",[203,1223,1225],{"id":1224},"joining-server-timing-with-traces","Joining Server-Timing with Traces",[15,1227,1228,1229,1232],{},"For deeper analysis, send a trace or request id as a Server-Timing entry (",[33,1230,1231],{},"trace;desc=4bf92f3577b34da6",") and include it in the RUM beacon. Your observability platform can then link a specific slow page view to the distributed trace that produced it — database spans, downstream service calls, queue time — without guesswork. Sample this: emitting trace ids on every response is cheap, but storing the link for every beacon may not be; keep it for slow page views (TTFB above a threshold) and a small random sample of the rest. Be aware that exposing a trace id is generally harmless but should not grant access to anything on its own.",[203,1234,1236],{"id":1235},"naming-conventions-that-survive","Naming Conventions That Survive",[15,1238,1239,1240,402,1242,402,1244,402,1247,402,1249,402,1252,1255,1256,1258],{},"Server-Timing entries become dashboard dimensions, so treat their names as an API. Use lowercase, short, stable names (",[33,1241,715],{},[33,1243,718],{},[33,1245,1246],{},"auth",[33,1248,1090],{},[33,1250,1251],{},"edge",[33,1253,1254],{},"app","), never per-request values in names, and put variable data in ",[33,1257,68],{}," only when it is low-cardinality (cache status, region code). Document the list next to the middleware that emits it. When a new phase is added, add a new name rather than redefining an existing one — otherwise historical comparisons silently change meaning. Finally, keep the total header small: a dozen entries is plenty, and very long headers can be truncated by intermediaries.",[203,1260,1262],{"id":1261},"faq","FAQ",[1264,1265,1268,1272],"details",{"className":1266},[1267],"faq-item",[1269,1270,1271],"summary",{},"Does Server-Timing work with HTTP caching?",[15,1273,1274],{},"The header is cached with the response, so a cache hit replays the origin's original timings — misleading if you read them as \"this request's\" database time. Add a cache-status entry at the edge on every response, and interpret origin timings only when the status is a miss.",[1264,1276,1278,1281],{"className":1277},[1267],[1269,1279,1280],{},"Can Server-Timing be sent after the body starts streaming?",[15,1282,1283],{},"As a trailer, in principle, but browser support for Server-Timing trailers has been limited. With streaming SSR, emit what you know before the first byte (cache status, routing, data fetch for the shell) in the header and record later phases server-side only.",[1264,1285,1287,1290],{"className":1286},[1267],[1269,1288,1289],{},"Does it add measurable overhead?",[15,1291,1292,1293,239],{},"A few dozen bytes per response and negligible CPU. The main cost is discipline: keeping names stable so dashboards do not break, and keeping sensitive details out of ",[33,1294,68],{},[1264,1296,1298,1301],{"className":1297},[1267],[1269,1299,1300],{},"Can third-party APIs send Server-Timing that my RUM can read?",[15,1302,1303,1304,1306,1307,1309,1310,1313],{},"Only if they include the header and a ",[33,1305,238],{}," header that allows your origin. Without ",[33,1308,238],{},", cross-origin ",[33,1311,1312],{},"serverTiming"," arrays are empty in the browser. For your own APIs on separate hostnames, add both headers; for vendors, ask — some already provide useful timings.",[203,1315,1317],{"id":1316},"related","Related",[208,1319,1320,1327,1334],{},[211,1321,1322,1326],{},[19,1323,1325],{"href":1324},"\u002Fnetwork-protocol-optimization\u002Ftime-to-first-byte-optimization\u002Fdiagnosing-slow-ttfb-with-server-timing\u002F","Diagnosing slow TTFB with Server-Timing"," — the server-side optimisation workflow.",[211,1328,1329,1333],{},[19,1330,1332],{"href":1331},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fdiagnosing-cdn-cache-misses-from-response-headers\u002F","Diagnosing CDN cache misses from response headers"," — raising the hit ratio the cache-status entry measures.",[211,1335,1336,1340],{},[19,1337,1339],{"href":1338},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsegmenting-rum-by-device-and-connection\u002F","Segmenting RUM by device and connection"," — combining server and client dimensions.",[1342,1343,1345],"script",{"type":1344},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Expose Backend Timing with Server-Timing Headers\",\n  \"description\": \"How to send backend phase timings to the browser with Server-Timing, collect them in RUM, and use them to explain TTFB and LCP regressions.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Emit Server-Timing from the origin\",\n      \"text\": \"Time the phases you control and add them as entries.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Add edge timing and cache status\",\n      \"text\": \"Most CDNs can append their own entries (cache status, edge processing time) via configuration or edge code, so you see where in the chain time was spent.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Collect entries in RUM\",\n      \"text\": \"Expected outcome: each LCP beacon carries the server phases that produced its TTFB.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Analyse TTFB by server phase and cache status\",\n      \"text\": \"Group field TTFB by cdn-cache status and template, and plot db and render durations at p75 for misses.\"\n    }\n  ]\n}\n",[1342,1347,1348],{"type":1344},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Expose Backend Timing with Server-Timing Headers\",\n  \"description\": \"How to send backend phase timings to the browser with Server-Timing, collect them in RUM, and use them to explain TTFB and LCP regressions.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers\u002F\"\n  }\n}\n",[1342,1350,1351],{"type":1344},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Core Web Vitals & Measurement\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"RUM Beacons & Field Data Collection\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Exposing Backend Timing with Server-Timing Headers\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers\u002F\"\n    }\n  ]\n}\n",[1353,1354,1355],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":371,"searchDepth":384,"depth":384,"links":1357},[1358,1359,1360,1366,1367,1368,1369,1370],{"id":205,"depth":384,"text":206},{"id":242,"depth":384,"text":243},{"id":348,"depth":384,"text":349,"children":1361},[1362,1363,1364,1365],{"id":353,"depth":423,"text":354},{"id":722,"depth":423,"text":723},{"id":930,"depth":423,"text":931},{"id":1083,"depth":423,"text":1084},{"id":1210,"depth":384,"text":1211},{"id":1224,"depth":384,"text":1225},{"id":1235,"depth":384,"text":1236},{"id":1261,"depth":384,"text":1262},{"id":1316,"depth":384,"text":1317},"How to send backend phase timings to the browser with Server-Timing, collect them in RUM, and use them to explain TTFB and LCP regressions.","md",{"slug":1374,"type":1375,"breadcrumb":1376,"datePublished":1384,"dateModified":1384},"exposing-backend-timing-with-server-timing-headers","article",[1377,1380,1381,1382],{"name":1378,"url":1379},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1383},"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers\u002F","2026-10-06",true,"\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers",{"title":5,"description":1388},"Add Server-Timing headers for cache status, database and render time, read them in RUM via PerformanceServerTiming, and explain slow TTFB from the field.","core-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fexposing-backend-timing-with-server-timing-headers\u002Findex","VltZdS2rULzJqRGLBCO3qJdkcOIoL0iM1aDdg4DL9N8",[1392,1396],{"title":1393,"path":1394,"stem":1395},"CrUX vs Your Own RUM","\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fcrux-vs-your-own-rum-why-the-numbers-differ","core-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fcrux-vs-your-own-rum-why-the-numbers-differ\u002Findex",{"title":1397,"path":1398,"stem":1399},"Sampling RUM Data Without Biasing p75","\u002Fcore-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsampling-rum-data-without-biasing-p75","core-web-vitals-measurement\u002Frum-beacons-and-field-data-collection\u002Fsampling-rum-data-without-biasing-p75\u002Findex",1791308076878]