completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Time to first byte

Redirects, connection setup, and server time: what TTFB includes and what a good value is. A shop on HTTPS behind a proxy that adds a real round trip, measured in Chrome with web-vitals' attribution: every phase, four server times, a redirect, a warm connection, and what DevTools throttling misses.

By the end, you can
  • 01
    Take a slow first byte apartSplit TTFB into web-vitals' five parts, and count the round trips that a redirect and a new connection add before the server even starts.
  • 02
    Judge a TTFBRate it with web.dev's 0.8 and 1.8 second limits, and explain why a lab test and real visitors get different numbers for the same page.
  • 03
    See what it holds upShow that the first paint and the largest paint wait for the first byte, and why sending part of the page early makes TTFB look better than the page is.

The first byte of the page

Asha opens the Mint tea shop's page. About three quarters of a second later, the shop appears on her screen. For most of that time, the browser had nothing new to draw. It was waiting for the page itself to start arriving.

The page arrives as HTML, a little at a time. The very first piece to come back from the server is the first byte. The wait for it, from the start of the visit, is called Time to First Byte, or TTFB. web.dev is the Chrome team's site for web developers. It calls TTFB "a metric that measures the time between starting navigating to a page and when the first byte of a response begins to arrive."

A slow TTFB is easy to blame on the server. Was it the server here? The course recorded Asha's visit, with a server that spent 300 ms building the page:

How much of the wait was the server?Pop out in the code editor (opens in a new tab)JavaScript
// Asha's visit to the shop, the middle of 3 visits. Times in ms.const ttfb = 519.4; // when the first byte of the page arrivedconst serverWork = 300; // how long the server spent building the page console.log(`The first byte arrived after ${ttfb} ms.`);console.log(`The server's work was ${serverWork} ms of that.`);console.log(`The other ${(ttfb - serverWork).toFixed(1)} ms went on getting there.`);
  • Line 1 is a comment. The course timed three visits, and this is the middle one, the median. Times are in milliseconds (ms), thousandths of a second.
  • Line 2 is the recorded TTFB. Line 3 is how long the server worked on the page.
  • Lines 5 to 7 print The first byte arrived after 519.4 ms., The server's work was 300 ms of that. and The other 219.4 ms went on getting there.

So the server was only part of the wait. More than 200 ms went on something else, and the rest of this lesson finds out what. Here is the whole visit at once, from the start to the first text or image on the screen:

Asha's visit straight to the shop, the middle of 3. The browser's own checks took 4.9 ms, opening the connection 110.9 ms, and the request with the server's 300 ms of work 403.6 ms. The first byte arrived at 519.4 ms. Loading the rest and painting took until FCP, at 764 ms.TTFB: the wait for the first bytethe rest of the pageconnectrequest + serverload and paint0 msfirst byte, 519.4 msFCP, 764 ms
TTFB stops at the first byte. Shaded: the request, which holds the server's 300 ms of work. The page still had to load and paint after that.

Notice where TTFB stops. It stops at the first byte, not when the page is ready. After it, the browser still had to load the page's stylesheet and a script, and then paint. The first text or image appeared at 764 ms. That moment is called First Contentful Paint, or FCP.

Real-life analogyTiming a takeaway order

You order at a takeaway counter, and a friend times how long it takes. They don't wait for the whole bag to be packed. They stop the clock the moment the first dish is handed over. Everything before that counts: walking in, queueing, ordering, and the cooking.

In real life: Walking into the shop: the stopwatch starts
In JavaScript: The visit starts, at 0 ms
In real life: The first dish handed over the counter
In JavaScript: The first byte of the HTML: TTFB stops here
In real life: The rest of the bag being packed
In JavaScript: The rest of the HTML, the stylesheet, the scripts and the pictures
In real life: Your friend with the stopwatch
In JavaScript: The web-vitals library, which measures TTFB on the page
The main idea

TTFB is the wait from the start of a visit to the first byte of the page. It counts everything before that byte: every trip to the server and back, and the server's work. Nothing on the page can appear until it ends.

How the course timed it

To see where the time goes, the course built the shop from this story and timed it in Chrome. The set-up:

  • The shop is one small page: a heading, a photo of mint tea, and a link. It's served over HTTPS, like a real site.
  • Between Chrome and the shop sits a delay proxy: a small go-between program on the same computer. It holds every message for 50 ms on its way, in each direction.
  • So each round trip, a message there and its answer back, takes 100 ms, as on a real network. Latency vs bandwidth showed why round trips matter so much.
  • The server waits 300 ms before it answers, as if it were building the page.
  • Chrome loaded each kind of visit 3 times. Unless the visit says otherwise, each one started with no connection open.

The page measured itself with the attribution build of the web-vitals library, from The web-vitals library. Attribution means the library also reports where the time went, not just how much there was.

What TTFB includes

Back to the question. The server worked for 300 ms, but the first byte took 519.4. Where did the other 219.4 ms go? Picture the takeaway on a busy evening.

Real-life analogyBefore the kitchen sees your order

On a busy evening, much of your wait happens before the kitchen even hears your order. You find the shop, queue at the door, and walk up to the counter. The cooks only start when you say what you want. Then the first dish still has to come back across the counter to you.

In real life: Finding the shop
In JavaScript: DNS: turning the shop's name into an address
In real life: Queueing at the door
In JavaScript: Opening the connection: the TCP and TLS handshakes
In real life: Giving your order at the counter
In JavaScript: The request travelling to the server
In real life: The kitchen cooking
In JavaScript: The server's work
In real life: The first dish coming back over the counter
In JavaScript: The first byte travelling back

web.dev lists the same steps. In its words, TTFB is the sum of:

  • "Redirect time"
  • "Service worker startup time (if applicable)"
  • "DNS lookup"
  • "Connection and TLS negotiation"
  • "Request, up until the point at which the first byte of the response has arrived"

A service worker is a script a site can install in the browser, to answer some requests itself. "Negotiation" means the handshakes: the hello messages that open a connection before any page data moves.

Five parts, from web-vitals

The web-vitals library splits TTFB into five parts. It works them out from the navigation entry, the page's list of times from The navigation timeline. Each part runs from one of those times to the next:

  • waitingDuration: from the start of the visit until the browser begins fetching the page. Redirects land here. The library's README says that even without one, "it's generally not zero."
  • cacheDuration: the browser checking its HTTP cache, its store of files saved from earlier visits.
  • dnsDuration: looking up the server's address.
  • connectionDuration: opening the connection, with its handshakes.
  • requestDuration: from the moment the connection is ready until the first byte comes back. The README says it "includes network time as well as server processing time."

Here are the five parts of Asha's visit:

The five parts of one TTFBPop out in the code editor (opens in a new tab)JavaScript
// web-vitals' five parts of TTFB for the same visit, in ms.const phases = { waitingDuration: 0.3, cacheDuration: 4.6, dnsDuration: 0, connectionDuration: 110.9, requestDuration: 403.6 }; for (const [phase, ms] of Object.entries(phases)) console.log(`${phase}: ${ms} ms`);const ttfb = Object.values(phases).reduce((sum, ms) => sum + ms, 0);console.log(`TTFB: ${ttfb.toFixed(1)} ms`);
  • Line 2 holds the five parts web-vitals recorded for the visit.
  • Line 4 prints each one: waitingDuration: 0.3 ms, cacheDuration: 4.6 ms, dnsDuration: 0 ms, connectionDuration: 110.9 ms and requestDuration: 403.6 ms.
  • Line 5 adds them up, and line 6 prints TTFB: 519.4 ms. The five parts make the whole TTFB.

Now read them one at a time. Two parts are big:

  • Connection, 110.9 ms. That's the TLS handshake, where the browser and the server agree on a private code (TLS without the wait). TLS 1.3 does it in what its standard, RFC 8446, calls a "1-RTT handshake": one round trip. All 72 connections in the recording used TLS 1.3.
  • Request, 403.6 ms. That's the server's 300 ms, plus one round trip: the request travelling there, and the first byte travelling back.

The other three are small:

  • DNS was 0 ms. Chrome was told in advance that shop.test lives on this computer, so it didn't look anything up.
  • Waiting and cache were a few milliseconds of the browser's own work.

One step is missing: TCP's handshake. This visit used TCP, and a TCP connection opens with its own hello, one round trip, before TLS can start (TCP connections and slow start). Here the proxy sat on the same computer and answered that hello at once. Across a real network, the same visit would pay one more round trip.

Why it matters: on a new connection, the first byte waited for two round trips plus the server's work. The kitchen can't start until the order reaches it, and a faster kitchen can't shorten the trips.

Where the clock starts

Your friend at the takeaway could start the stopwatch at two different moments. The choice changes the number a lot.

Real-life analogyFrom the door, or from the counter

Two friends time the same order. One starts the stopwatch as you walk through the door. The other starts it when you reach the counter and order. The second friend gets a smaller number. But you were waiting the whole time.

In real life: Walking through the door
In JavaScript: The start of the visit: startTime, which is 0
In real life: Reaching the counter
In JavaScript: The connection is ready, and the request can go
In real life: The first friend's number
In JavaScript: TTFB
In real life: The second friend's number
In JavaScript: web-vitals' requestDuration

TTFB is always the friend at the door. The web-vitals README says the TTFB metric "starts from the page's time origin". The time origin is the start of the visit, 0 ms. So TTFB "includes time spent on DNS lookup, connection negotiation, network latency, and server processing time." The friend at the counter gets the request part. Here are both clocks for Asha's visit:

Two clocks, one visitPop out in the code editor (opens in a new tab)JavaScript
// The same visit, in ms from the moment it started.const connectEnd = 115.8; // the connection was ready, so the request could goconst responseStart = 519.4; // the first byte arrived console.log(`Counted from the start of the visit: ${responseStart} ms`);console.log(`Counted from the open connection: ${(responseStart - connectEnd).toFixed(1)} ms`);
  • Line 2 is when the connection was ready: the waiting, cache, DNS and connection parts added up. The navigation entry calls that moment connectEnd.
  • Line 3 is when the first byte arrived. The navigation entry calls it responseStart.
  • Line 5 prints Counted from the start of the visit: 519.4 ms. That's TTFB.
  • Line 6 prints Counted from the open connection: 403.6 ms. That's the request part.

Read this table across. Each row is one part of a visit, and whether each clock counts it:

Two clocks for the same wait
Part of the visitDoes TTFB count it?Does the request part count it?
A redirect before the pageYes. It lands in the waiting part.No. It happens before the real request.
Checking the cache and looking up the addressYes.No.
Opening the connectionYes. On a new connection, that's the handshakes.No.
The request's trip to the server and backYes.Yes.
The server's workYes.Yes.
The rest of the HTMLNo. TTFB stops at the first byte.No. The request part stops there too.

Two kinds of visit are timed differently. The web-vitals library recorded both:

  • A page restored from the back/forward cache, the copy the browser keeps in memory when you leave a page. Nothing comes from the network, so web-vitals reports a TTFB of 0.
  • A prerendered page, loaded out of sight before the visitor asked for it. Its clock starts when the visitor opens it, at the navigation entry's activationStart. web-vitals' source code says TTFB "should be relative to page activation rather than navigation start if the page was prerendered." If the first byte had already arrived by then, it reports 0.

In that lesson, the prerendered shop was opened 2,462.7 ms after its hidden load began, and its TTFB was 0. It's like a friend who ordered for you before you arrived: when you walk in, your food is already on the table.

Why it matters: a TTFB of 0 isn't a broken measurement. It's a visit where the visitor didn't wait for the network at all.

A redirect is a whole extra trip

A redirect is the sign on the shop door from The navigation timeline: "We've moved two doors down." The browser asks for one address, the server answers with another, and the browser asks again. Links from a link shortener work this way.

The course sent Asha through one. Her link pointed at /go on the shop. That answered with the status code 302 and the shop page's address:

What one redirect costPop out in the code editor (opens in a new tab)JavaScript
// TTFB in ms, the middle of 3 visits each.const straight = 519.4; // a link straight to the shopconst throughRedirect = 618.1; // a short link that redirects to the shop console.log(`The redirect added ${(throughRedirect - straight).toFixed(1)} ms.`);
  • Lines 2 and 3 are the recorded TTFBs: straight to the shop, and through the short link.
  • Line 5 prints The redirect added 98.7 ms.

That's about one round trip. Where did it go? web-vitals' parts show it:

Where the redirect's time shows upPop out in the code editor (opens in a new tab)JavaScript
// web-vitals' parts of TTFB for the visit through the redirect, in ms. Its cache and DNS parts were 0.const phases = { waitingDuration: 209.9, connectionDuration: 0, requestDuration: 408.2 }; console.log(`Waiting, while the redirect happened: ${phases.waitingDuration} ms`);console.log(`Connecting for the real request: ${phases.connectionDuration} ms`);console.log(`The real request: ${phases.requestDuration} ms`);
  • Line 1 says the cache and DNS parts were 0 on this visit.
  • Line 2 holds the other three recorded parts.
  • Lines 4 to 6 print Waiting, while the redirect happened: 209.9 ms, Connecting for the real request: 0 ms and The real request: 408.2 ms.

The redirect happened before the real request, so all of its time is in the waiting part. That waiting holds two round trips: one to open the connection, and one to ask /go and read its answer. Then the real request found the connection already open, so its connection part was 0.

So the handshake didn't disappear. It moved, from the connection part to the waiting part. The redirect's own cost was the one extra round trip. This picture puts the two visits side by side:

web-vitals' parts of TTFB, the middle of 3 visits each. Straight to the page: TTFB 519.4 ms, cache 4.6 ms, connect 110.9 ms, request 403.6 ms. Through a redirect: TTFB 618.1 ms, waiting 209.9 ms, request 408.2 ms.Straight to the pageconnectrequest519.4 msThrough a redirectwaitingrequest618.1 ms
Each card is one of web-vitals' parts, drawn to scale. Shaded: the request, which holds the server's 300 ms. Through the redirect, the connection was opened while waiting, so the real request found it open.

Why it matters: every redirect costs at least one round trip, before the server has even heard which page you want. The longer the round trip, the more it costs. Redirects shows how to find chains of them and remove them.

Every recorded visit

Here are all the kinds of visit the course recorded. Pick one to see its five parts. The title gives its TTFB, how web-vitals rated it, its FCP, and its LCP, Largest Contentful Paint: the moment the biggest text or image was painted.

Playground: what TTFB is made of
PartTime
  1. Waiting0.3 ms
  2. Cache4.6 ms
  3. DNS0 ms
  4. Connection110.9 ms
  5. Request403.6 ms
Try it yourself
Straight to the page: TTFB 519.4 ms, good; FCP 764 ms, LCP 764 ms

The middle of 3 visits. Each row is one of web-vitals' five parts of TTFB, and the five add up to it. The server worked for 300 ms. Every round trip took 100 ms.

Recorded in Chrome 156 on Apple M1 Pro, over HTTPS (TLSv1.3), through a proxy that holds every message for half a round trip.

The 0.8 second guideline

Asha's TTFB was 519.4 ms. Is that good? web.dev gives three bands:

  • Good: 0.8 seconds or less.
  • Needs improvement: more than 0.8 seconds, up to 1.8.
  • Poor: more than 1.8 seconds.

Why 0.8? The first byte comes before the first paint. So web.dev wants it early enough "that the 75th percentile of users experience an FCP within the 'good' threshold." The 75th percentile is the time that three visits in four stay at or under (Averages lie: reading distributions). The first byte has to leave room for everything after it.

The web-vitals library uses the same two limits: its source sets them at 800 and 1,800 ms. The course tried four amounts of server work, over the same 100 ms round trips:

Four servers, ratedPop out in the code editor (opens in a new tab)JavaScript
// TTFB in ms, the middle of 3 visits, for four amounts of server work.const ttfb = { 0: 208.3, 300: 512, 900: 1111.6, 1800: 2010.7 }; const rating = (ms) => (ms <= 800 ? "good" : ms <= 1800 ? "needs improvement" : "poor");for (const [server, ms] of Object.entries(ttfb)) console.log(`Server ${server} ms: TTFB ${ms} ms, ${rating(ms)}`);
  • Line 2 holds the recorded TTFBs, one for each amount of server work.
  • Line 4 rates a value with web.dev's limits.
  • Line 5 prints one line for each: Server 0 ms: TTFB 208.3 ms, good, Server 300 ms: TTFB 512 ms, good and Server 900 ms: TTFB 1111.6 ms, needs improvement. The last is Server 1800 ms: TTFB 2010.7 ms, poor.

web-vitals gave every recorded visit the same rating as line 4 does. The 300 ms row says 512 ms, not 519.4. These were separate visits, and separate visits vary a little.

A floor that round trips set

Look at the first row. With no server work at all, the first byte still took 208.3 ms. That's the two round trips: the handshake and the request. It's a floor. No amount of server speed can take TTFB below it on this network.

Why it matters: to get under the floor, you need fewer round trips, or shorter ones. A connection that's already open saves the handshake. A server closer to the visitor makes every trip shorter; CDNs and the edge covers that.

A guide, not a Core Web Vital

TTFB isn't one of the Core Web Vitals, the main measures of a page's experience. web.dev says "it's not absolutely necessary that sites meet the 'good' TTFB threshold, provided that it doesn't impede their ability to score well on the metrics that matter." To impede is to get in the way.

Think of 0.8 seconds like the cooking time printed on a packet: a guide, while the real test is whether the meal is ready. web.dev gives two cases:

  • A page that JavaScript builds in the browser, such as a single-page app, needs the lowest TTFB it can get. Its script can only start building once the HTML has arrived.
  • A page that the server builds whole "could have a higher TTFB, but better FCP and LCP values" than one built in the browser.

So read TTFB together with the paints that come after it. The last section of this lesson shows how closely they move.

Lab TTFB vs field TTFB

A lab test is one you run yourself, on one computer, with set conditions. Field data comes from real visitors (Lab data vs field data). The two often disagree about TTFB. Usually both are right: they timed different visits. The course recorded two reasons.

A tool that hides the network's wait

Chrome's DevTools can slow the network down for testing. That's called DevTools throttling. The course set it to add 100 ms of latency, the same wait as the delay proxy, and loaded the same page:

The same wait, added two waysPop out in the code editor (opens in a new tab)JavaScript
// TTFB in ms, the middle of 3 visits each. Both ways added 100 ms of latency.const server = 300;const ttfb = { "Through the delay proxy": 519.4, "With DevTools throttling": 303.9 }; for (const [how, ms] of Object.entries(ttfb)) {  console.log(`${how}: ${ms} ms, of which ${(ms - server).toFixed(1)} ms was not the server`);}
  • Line 2 is the server's work. Line 3 holds the two recorded TTFBs.
  • Line 6 takes the server's work away from each one. It prints Through the delay proxy: 519.4 ms, of which 219.4 ms was not the server and With DevTools throttling: 303.9 ms, of which 3.9 ms was not the server.

Under DevTools throttling, TTFB was the server's 300 ms and almost nothing else. The 100 ms of latency never reached it. Lighthouse's docs call this kind "Request-level throttling". They explain: "In real mobile connectivity, latency affects things at the packet level rather than the request level."

Choosing test conditions found where the wait went: DevTools held back the body of the response, not its first bytes. Why it matters: never read the network's share of TTFB from a DevTools-throttled test. Use a real network, or a proxy like this one.

Visitors come by different roads

A lab test often loads one address, straight. Real visitors don't all do that. Some are already on the site, so a connection is open. Some arrive fresh. Some come through a short link in an email. It's like timing the walk to the shop from the house next door, when your customers come from all over town. The course timed the same page all three ways:

The same page, reached three waysPop out in the code editor (opens in a new tab)JavaScript
// TTFB in ms, the middle of 3 visits each: the same page, reached three ways.const server = 300;const ttfb = { "Already connected": 406.8, "A new connection": 519.4, "Through a redirect": 618.1 }; for (const [how, ms] of Object.entries(ttfb)) {  console.log(`${how}: ${ms} ms, of which ${(ms - server).toFixed(1)} ms was not the server`);}
  • Line 3 holds the three recorded TTFBs. "Already connected" means Chrome loaded the shop, went to another page on the same site, and then opened the shop again. The connection was still open.
  • Line 6 prints one line for each. The first is Already connected: 406.8 ms, of which 106.8 ms was not the server.
  • The second is A new connection: 519.4 ms, of which 219.4 ms was not the server.
  • The third is Through a redirect: 618.1 ms, of which 318.1 ms was not the server.

Now count the round trips. Already connected: one, for the request. A new connection: two, with the handshake. Through a redirect: three. Each one added about 100 ms. Same page, same server, and TTFB went from 406.8 to 618.1 ms.

Median TTFB of the same page, 300 ms of server work and a 100 ms round trip, measured four ways: DevTools throttling 303.9 ms, A new connection 519.4 ms, Already connected 406.8 ms, Through a redirect 618.1 ms.server 300 msDevTools throttling303.9 msA new connection519.4 msAlready connected406.8 msThrough a redirect618.1 ms
Dashed: the server's 300 ms. The part of each bar past the line wasn't the server's work. Shaded: DevTools throttling, whose latency never reached TTFB.

web.dev warns about exactly this. Lab tools "are often measured using the final URL therefore missing this extra delay." And when field TTFB is much larger than lab TTFB, it "indicates issues that aren't apparent in the lab run, such as use of server-side caching, redirects, or network differences."

Server-side caching means the server keeps ready-made copies of pages, so it doesn't build them every time. web.dev warns that testing a cached page "often shows no TTFB issues". Meanwhile, older or less visited pages may not be cached at all, so their TTFB can be higher.

Why it matters: trust field TTFB for what visitors get, and use the lab to find out why. Make the lab copy the field: test the links visitors really use, on a new connection, over a real network.

TTFB's effect on every later metric

web.dev says TTFB "precedes every other meaningful user experience metric such as First Contentful Paint (FCP) and Largest Contentful Paint (LCP). This means that high TTFB values add time to the metrics that follow it." To precede is to come before. The browser can't paint a page it hasn't received.

The four server times show it:

FCP waits for the first bytePop out in the code editor (opens in a new tab)JavaScript
// TTFB and FCP in ms, the middle of 3 visits, for four amounts of server work.const visits = { 0: [208.3, 452], 300: [512, 760], 900: [1111.6, 1348], 1800: [2010.7, 2256] }; for (const [server, [ttfb, fcp]] of Object.entries(visits)) {  console.log(`Server ${server} ms: FCP at ${fcp} ms, ${(fcp - ttfb).toFixed(1)} ms after the first byte`);}
  • Line 2 holds the recorded TTFB and FCP for each amount of server work.
  • Line 5 prints when FCP came, and how long after the first byte.
  • It prints Server 0 ms: FCP at 452 ms, 243.7 ms after the first byte and Server 300 ms: FCP at 760 ms, 248.0 ms after the first byte.
  • Then Server 900 ms: FCP at 1348 ms, 236.4 ms after the first byte and Server 1800 ms: FCP at 2256 ms, 245.3 ms after the first byte.

The gap after the first byte hardly changed: about a quarter of a second every time. So the extra server work went straight into FCP. Going from 0 to 1,800 ms of server work moved FCP by 1,804 ms.

Median TTFB and FCP of 3 cold runs over a 100 ms round trip: server 0 ms, TTFB 208.3 ms, FCP 452 ms; server 300 ms, TTFB 512 ms, FCP 760 ms; server 900 ms, TTFB 1,111.6 ms, FCP 1,348 ms; server 1,800 ms, TTFB 2,010.7 ms, FCP 2,256 ms. web.dev's good threshold is 800 ms and poor starts above 1,800 ms.800 ms1,800 msServer 0 msTTFB 208.3 msFCP 452 msServer 300 msTTFB 512 msFCP 760 msServer 900 msTTFB 1,111.6 msFCP 1,348 msServer 1,800 msTTFB 2,010.7 msFCP 2,256 ms
Shaded: TTFB. Plain: FCP, the first text or image on screen. The dashed lines are web.dev's limits, 0.8 and 1.8 seconds. Each FCP bar is about a quarter of a second longer than its TTFB bar.

LCP did the same. In every recorded visit, the largest text or image was painted at the same moment as the first one, so LCP equalled FCP. web.dev calls an LCP of 2.5 seconds or less good. It also says LCP "includes any unload time from the previous page, connection set up time, redirect time, and other Time To First Byte (TTFB) delays".

So a TTFB above 2.5 seconds rules out a good LCP before the page has even arrived. Here, 1.8 seconds of server work already put LCP at 2,256 ms, close to the limit. Why it matters: TTFB isn't a Core Web Vital, but it uses up the others' time first. The four parts of LCP counts it as the first of LCP's four parts.

When the first byte comes early

TTFB times one byte. So it can look better on paper while the content still comes late.

Real-life analogyBread first, while the main course cooks

A restaurant brings bread to your table while your main course cooks. If your friend stops the stopwatch at the first food on the table, the kitchen looks very fast. But you're still waiting for the meal you came for.

In real life: The bread, brought out at once
In JavaScript: The <head> of the HTML, sent before the rest is ready
In real life: The stopwatch stopping at the bread
In JavaScript: TTFB ending at that first byte
In real life: The main course, still cooking
In JavaScript: The page's content, still being built on the server

Some servers do exactly this. web.dev explains that they can send part of the response "before the main body is available". That part can be "just the HTTP headers", or the page's <head>. Sending what's ready so far is called flushing. Either way, the early part carries the first byte, so TTFB stops there. web.dev: "This is another reason why all these are measured as responseStart and so TTFB."

A 103 Early Hints response is another early message. It tells the browser which files to start fetching while the page is built. web.dev says "The 103 Early Hints counts as the 'first bytes'" too. 103 Early Hints covers it.

Sending early is useful. web.dev: "There is real value in sending back data early if the full response is going to take some more time." The browser can start on the files the head names while the server works. But TTFB then says less about when the content arrived.

Why it matters: before you compare two TTFBs, check whether either server sends early. And always read TTFB next to FCP and LCP. Faster server responses times a page that flushes its head early.

Which phase is it?

Each delay below happens in one part of a visit. Ask where it falls: before the real request, while connecting, between the order and the first dish, or after the first byte.

Which phase is it?
  • A 302 from a short link to the page
  • A tracking hop before the real URL
  • The TLS handshake on a new connection
  • Looking up the shop's address
  • 300 ms of building the page on the server
  • The request's own round trip
  • Downloading the hero photo
  • Reading the stylesheet
Try it yourself
0 of 8 correct

Sort each delay by when it happens.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "TTFB is the server's time." No. It counts from the start of the visit. In Asha's visit, the server's work was 300 ms of a 519.4 ms TTFB.
  • "A DevTools-throttled test shows the TTFB visitors get." No. With 100 ms of latency, it reported 303.9 ms: the server's work and almost nothing else.
  • "Lab TTFB is what visitors get." No. Visitors arrive through redirects, on new or open connections. The same page measured anywhere from 406.8 to 618.1 ms.
  • "TTFB is a Core Web Vital." No. web.dev calls its limits a rough guide. What counts is whether it gets in the way of FCP and LCP.
  • "A slow TTFB only delays the HTML." No. FCP and LCP moved with it. 1.8 seconds of server work put FCP at 2,256 ms instead of 452.
  • "A low TTFB means the page arrived quickly." No. TTFB stops at the first byte. A server that sends its head early can have a low TTFB and still show its content late.
  • "A TTFB of 0 means something broke." No. web-vitals reports 0 for a page restored from the back/forward cache, and for a prerendered page whose first byte came before the visitor opened it.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upA connection that was already open

These are the five parts of a visit on a connection that was already open. What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// web-vitals' five parts of TTFB for a visit on a connection that was already open, in ms.
const phases = { waitingDuration: 0.6, cacheDuration: 0, dnsDuration: 0, connectionDuration: 0, requestDuration: 406.2 };

for (const [phase, ms] of Object.entries(phases)) console.log(`${phase}: ${ms} ms`);
const ttfb = Object.values(phases).reduce((sum, ms) => sum + ms, 0);
console.log(`TTFB: ${ttfb.toFixed(1)} ms`);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upA stricter budget

    Your team sets its own, stricter TTFB budget: 500 ms. What does the second line print now?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // TTFB in ms, the middle of 3 visits, for four amounts of server work.
    const ttfb = { 0: 208.3, 300: 512, 900: 1111.6, 1800: 2010.7 };
    
    const rating = (ms) => (ms <= 500 ? "within the 500 ms budget" : "over budget");
    for (const [server, ms] of Object.entries(ttfb)) console.log(`Server ${server} ms: TTFB ${ms} ms, ${rating(ms)}`);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeA redirect's first paint

      In the playground, what was the median FCP of the visit through a redirect, in ms?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeA slow server's request part

        In the playground, how long was the request part of the visit with 900 ms of server work, in ms?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWhen the report comes

          The web-vitals README says onTTFB() doesn't call your function the moment the first byte arrives. When does it call it?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeA service worker's start-up

            The shop adds a service worker. In web-vitals' TTFB attribution, which of the five parts will the worker's start-up time land in?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, count the round trips, add the server's work, and ask where the clock started.

              Time to first byte quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does the last line of the five-parts example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // web-vitals' five parts of TTFB for the same visit, in ms.
                const phases = { waitingDuration: 0.3, cacheDuration: 4.6, dnsDuration: 0, connectionDuration: 110.9, requestDuration: 403.6 };
                
                for (const [phase, ms] of Object.entries(phases)) console.log(`${phase}: ${ms} ms`);
                const ttfb = Object.values(phases).reduce((sum, ms) => sum + ms, 0);
                console.log(`TTFB: ${ttfb.toFixed(1)} ms`);

                Choose an answer to see the explanation.

              2. Question 2 of 8What does the third line of the three-ways example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // TTFB in ms, the middle of 3 visits each: the same page, reached three ways.
                const server = 300;
                const ttfb = { "Already connected": 406.8, "A new connection": 519.4, "Through a redirect": 618.1 };
                
                for (const [how, ms] of Object.entries(ttfb)) {
                  console.log(`${how}: ${ms} ms, of which ${(ms - server).toFixed(1)} ms was not the server`);
                }

                Choose an answer to see the explanation.

              3. Question 3 of 8The server spends 300 ms on the page, and each round trip takes 100 ms. Why is TTFB above 500 ms on a new connection?

                Choose an answer to see the explanation.

              4. Question 4 of 8Lab TTFB looks fine, but TTFB from real visitors is poor. What could the lab be missing?

                Choose an answer to see the explanation.

              5. Question 5 of 8TTFB isn't a Core Web Vital. Why work on it anyway?

                Choose an answer to see the explanation.

              6. Question 6 of 8A visitor arrives through a short link that redirects to the page. What does the redirect add to TTFB?

                Choose an answer to see the explanation.

              7. Question 7 of 8Chrome prerendered a page, and the visitor opened it later. Where does web-vitals start that page's TTFB clock?

                Choose an answer to see the explanation.

              8. Question 8 of 8A server sends the page's <head> at once, and the rest only when a slow database query finishes. What happens to TTFB?

                Choose an answer to see the explanation.

              Key takeaways

              • TTFB is the wait from the start of a visit to the first byte of the page. It stops there, long before the page is ready.
              • It adds up redirects, the cache check, DNS, the connection, and the request with the server's work.
              • It counts from the start of the visit. A prerendered page counts from when it was opened, and a back/forward cache restore reports 0.
              • Good is 0.8 seconds or less, and poor is above 1.8. Round trips alone set a floor that no server can beat.
              • Field TTFB includes redirects and real networks. DevTools throttling leaves the network's wait out of TTFB.
              • FCP and LCP wait for the first byte. Sending the head early lowers TTFB, even when the content still comes late.

              Remember the one-liner.
              TTFB is every round trip before the answer, plus every millisecond of the server's work.

              Sources

              The facts on this page come from these docs and this code:

              Coming next: Faster server responses. The request part held the server's 300 ms. The next lesson opens it up: it times the server's own steps with Server-Timing, caches what it can, and sends the head early.

              CompleteFrontend Clear concepts. Working examples.