completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Redirects

Each redirect is a full round trip or more. Old links, the http-to-https and www hops, trailing slashes, meta refreshes and newsletter trackers, recorded in Chrome over a real round trip, with and without its automatic HTTPS upgrades, and how to cut each chain to one hop or none.

By the end, you can
  • 01
    Count what a redirect costsTell a redirect that costs one round trip from one that needs a whole new connection.
  • 02
    Find redirect chainsSpot them in DevTools, and in field data, where redirects through other sites aren't counted.
  • 03
    Remove themFix your links, redirect in one hop, send HSTS, and give newsletters the final URL.

What a redirect costs

Asha is reading an old blog post about green tea. At the bottom it says: "Buy it at the Mint tea shop." The link was written years ago: http://example.com/tea. She taps it. The tea page appears, but it feels slow.

The link still works. But the shop has moved its tea page since the post was written, so Asha's browser had to follow the old addresses, one by one. How much did that cost her? The course built the shop and timed her visit two ways: through the old link, and through a link to where the page lives now.

The same page, through two linksPop out in the code editor (opens in a new tab)JavaScript
// The middle one of 3 visits (the median), in ms: from Asha's click to the first byte of the tea page.const throughOldLink = 725.7; // http://example.com/teaconst straightToPage = 310.9; // https://www.example.com/tea/ console.log(`Through the old link: ${throughOldLink} ms`);console.log(`Straight to the page: ${straightToPage} ms`);console.log(`The old link cost ${(throughOldLink - straightToPage).toFixed(1)} ms more.`);
  • Line 1 is a comment. The course timed each visit three times and kept the middle one, the median. Times are in milliseconds (ms), thousandths of a second.
  • Lines 2 and 3 are the recorded TTFB, or time to first byte: the wait from Asha's click until the first byte of the tea page arrived. Time to first byte took it apart.
  • Lines 5 and 6 print Through the old link: 725.7 ms and Straight to the page: 310.9 ms.
  • Line 7 subtracts one from the other. It prints The old link cost 414.8 ms more.

So the old link more than doubled Asha's wait. Same shop, same page, same server. Where did the extra 414.8 ms go? Here is her visit through the old link, one request at a time:

Asha's old link, the median of 3 visits, from DevTools: four requests, one after another. 307 http://example.com/tea, inside Chrome (HttpsUpgrades): 1.0 ms; 301 https://example.com/tea, on a new connection: 310.0 ms; 301 https://www.example.com/tea, on a new connection: 310.9 ms; 200 https://www.example.com/tea/, on the open connection: 103.4 ms.http://example.com/tea307, 1.0 mshttps://example.com/tea301310.0 mshttps://www.example.com/tea301310.9 mshttps://www.example.com/tea/200103.4 ms
Four requests for one page, one after another. The first never left Chrome. The shaded ones each opened a new connection.

Asha's browser asked for the page four times. The first three answers each said "not here, try this address". Only the fourth got the page. Every request that reaches a server costs at least one round trip: a message there, and its answer back. On the course's network, one round trip took 100 ms.

But the four requests didn't cost the same. Two took about 310 ms each, one took about 100, and the first took 1. Why? The rest of this lesson answers that.

Real-life analogyA sign that points to another sign

You walk to your favourite tea shop. The door is locked, with a sign: "We've moved to number 12." You walk to number 12. Its door has a sign too: "Now at number 30." Nobody meant to waste your time. But every sign cost you a walk, and the walk to number 30 was the only one you needed.

In real life: The old door, with its sign
In JavaScript: The old address, like https://example.com/tea
In real life: "We've moved to number 12"
In JavaScript: A redirect: a status code like 301, and the new address
In real life: Walking to number 12
In JavaScript: The browser asking again, at the new address
In real life: Another sign at number 12
In JavaScript: A redirect chain: one redirect that leads to another

The navigation timeline called a redirect a sign on the shop door. Here is what it really is. A redirect is a server's answer that says "the page is at another address". It has two parts:

  • A status code that starts with 3, like 301. Every answer starts with a status code. 200 means "here it is". RFC 9110, the standard that defines HTTP, calls the codes in the 300s the redirection codes.
  • A Location header that holds the new address. A header is a line of extra information that comes with an answer.

Here is what the shop's old server sends back when it's asked for https://example.com/tea. Its other headers are left out:

A redirect, as the server sends itHTTP
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/tea
  • Line 1 is the status line. It gives the protocol, HTTP/1.1, then the status code 301 and its name, Moved Permanently.
  • Line 2 is the Location header. It holds the address to try next: https://www.example.com/tea.

The browser follows it at once. But nothing on the page can start until the last answer arrives.

The main idea

A redirect is a whole extra request, made before the page itself. On an open connection, it costs one round trip. To a new host, it also costs a new connection. So the fastest redirect is the one that never happens. Link to the final address, and when you can't, get there in one hop.

How the course timed it

The course built the shop and its neighbours on one computer, and timed them in Chrome. The set-up:

  • Four host names point at that computer: the shop, www.example.com; its old name, example.com; a newsletter's click counter, click.example.net; and a link shortener, link.example.org.
  • A delay proxy, a small go-between program, holds every message for 50 ms each way. So each round trip takes 100 ms, as on a real network.
  • It holds a new connection's first message for one more round trip. That's what the TCP handshake, the hello that opens a connection, costs on a real network.
  • The servers answer at once, so every millisecond you'll see is travel. They speak HTTP/1.1, so every host name needs a connection of its own.
  • There are no DNS lookups: Chrome was told where each name lives. On a real network, each new host name would also need a DNS lookup.
  • Chrome followed each route 3 times, each time in a fresh browser profile with nothing remembered. A second Chrome ran with its automatic HTTPS upgrades switched off.

Here are all the routes the course timed. Pick one to see its requests, how long each took, and whether it opened a new connection. The title adds up what Asha waited, counted from her click: the first byte, and the first paint, the moment the first text or image appeared.

Playground: follow the hops
RequestTime
  1. 307 http://example.com/teainside Chrome: HttpsUpgrades1 ms
  2. 301 https://example.com/teanew connection310 ms
  3. 301 https://www.example.com/teanew connection310.9 ms
  4. 200 https://www.example.com/tea/same connection103.4 ms
Try it yourself
Old http link, Chrome as shipped: first byte 725.3 ms after the click, first paint 988 ms

Each row is one request from the median run of 3, as DevTools saw it. The bar shows how long it took: until the next request started, or for the last one, until the page's first byte. In that run, the tea page's own TTFB was 725.7 ms, and its navigation entry counted 0 redirects.

Recorded in Chrome 156 (Apple M1 Pro). Every message waited 50 ms each way in a delay proxy, and the servers spoke HTTP/1.1.

Start with Old http link, Asha's route. Then pick Direct link: one request, and nothing else.

Kinds of redirects

A redirect's status code says what kind of move it is. This lesson covers the five that send a browser on to a new address: 301, 302, 303, 307 and 308. They differ in two ways:

  • Is the move for good? A permanent redirect says the old address is finished. A temporary one says "for now".
  • What happens to a form? Every request has a method. A GET asks for a page. A POST sends data, like a filled-in form, in the request's body.

Read each row of this table as one kind of redirect:

Five redirect status codes
Status codeIs the move for good?What happens to a form's POST?
301 Moved PermanentlyYes. The page has a new address for good.Browsers resend it as a GET, for historical reasons. The form's data is dropped.
308 Permanent RedirectYes. The page has a new address for good.It stays a POST, with the same data.
302 FoundNo. The page is somewhere else for now.Browsers resend it as a GET, for historical reasons. The form's data is dropped.
307 Temporary RedirectNo. The page is somewhere else for now.It stays a POST, with the same data.
303 See OtherNo. It sends the browser to a different page, such as a result page.It becomes a GET, on purpose. The form's data is dropped.

RFC 9110 lets a browser switch a POST to a GET after a 301 or 302, "for historical reasons". The Fetch standard, which browsers follow, says they do. So 307 and 308 were added later, and they promise to keep the method and data.

The 303 switches to a GET on purpose. MDN: it's used after a form "so that refreshing the result page doesn't re-trigger the operation". Asha can reload her order page without ordering twice.

For an ordinary visit, the method is GET, and all five move the browser the same way. What differs is what the browser remembers.

Real-life analogyA forwarding address the post office remembers

When you move house for good, you tell the post office. It writes your new address in its book, and from then on your letters go straight to the new house. When you're only away for a few weeks, the book doesn't change. Each letter goes to the old house first, and someone there sends it on.

In real life: Telling the post office you've moved for good
In JavaScript: A permanent redirect: 301 or 308
In real life: The post office writing it in its book
In JavaScript: The browser keeping the redirect in its cache
In real life: Later letters going straight to the new house
In JavaScript: Later visits skipping the old address
In real life: "I'm away for a few weeks"
In JavaScript: A temporary redirect: 302 or 307
In real life: Each letter going to the old house first
In JavaScript: The browser asking the old address on every visit

Browsers keep a cache, a store of answers from earlier visits. RFC 9110 says a 301 or a 308 is "heuristically cacheable". That means a browser may keep it even when the server didn't say for how long. A 302 or 307 can only be kept if the server sends cache headers that allow it, says RFC 9111. The course's server didn't send any.

The course sent four of the five from an old address, /old-tea, to the tea page. First, a visit with nothing remembered:

The first visit, for each kindPop out in the code editor (opens in a new tab)JavaScript
// The median of 3 first visits to an old address, /old-tea, for each kind of redirect: TTFB in ms.const firstVisit = { 301: 411.9, 302: 408.9, 307: 409.4, 308: 409.2 }; for (const [status, ttfb] of Object.entries(firstVisit)) console.log(`${status}: ${ttfb} ms`);
  • Line 2 holds the four recorded TTFBs.
  • Line 4 prints one line for each: 301: 411.9 ms, 302: 408.9 ms, 307: 409.4 ms and 308: 409.2 ms.

All four took between 408.9 and 411.9 ms. On a first visit, the status code made no difference. Each redirect cost the same one extra round trip on the open connection.

Then Chrome closed its connections and visited the old address again:

The repeat visit, for each kindPop out in the code editor (opens in a new tab)JavaScript
// The median of 3 repeat visits to /old-tea, after every connection had closed: TTFB in ms, and where the redirect came from.const repeatVisit = {  301: { ttfb: 305.8, fromCache: true },  302: { ttfb: 408.4, fromCache: false },  307: { ttfb: 408.9, fromCache: false },  308: { ttfb: 305.6, fromCache: true },}; for (const [status, { ttfb, fromCache }] of Object.entries(repeatVisit)) {  console.log(`${status}: ${ttfb} ms, ${fromCache ? "the redirect came from Chrome's cache" : "the server was asked again"}`);}
  • Lines 3 to 6 hold each status's recorded TTFB, and whether the redirect came from Chrome's cache.
  • Lines 9 to 11 print a line for each. The first is 301: 305.8 ms, the redirect came from Chrome's cache.
  • The middle two are 302: 408.4 ms, the server was asked again and 307: 408.9 ms, the server was asked again.
  • The last is 308: 305.6 ms, the redirect came from Chrome's cache.

Chrome kept the 301 and the 308. On the repeat visit, it read them from its cache in under 2 ms, without asking the old address at all. For the 302 and the 307, it asked the server again. The cache saved that round trip, about 100 ms.

Median TTFB of 3 runs through an old URL. 301, first visit: 411.9 ms. 301, repeat: 305.8 ms, the redirect from the cache. 302, first visit: 408.9 ms. 302, repeat: 408.4 ms. 307, first visit: 409.4 ms. 307, repeat: 408.9 ms. 308, first visit: 409.2 ms. 308, repeat: 305.6 ms, the redirect from the cache.301, first visit411.9 ms301, repeat305.8 ms302, first visit408.9 ms302, repeat408.4 ms307, first visit409.4 ms307, repeat408.9 ms308, first visit409.2 ms308, repeat305.6 ms
Each status has two bars: its first visit, then a repeat visit. Shaded: the redirect came from Chrome's cache, so the old address was never asked.

Why it matters: a permanent redirect is a promise. Once a browser has kept one, it may not ask your server again for a long time. So use 301 or 308 only for moves that are for good. For anything that might change back, use 302 or 307.

Redirects made by the page itself

A server isn't the only thing that can send a visitor on. A page can do it too, once it has arrived. There are two common ways:

Two ways a page sends you onHTML
<!-- Way one: a meta refresh. The browser follows it once the old page has loaded. -->
<meta http-equiv="Refresh" content="0; URL=https://www.example.com/tea/">

<!-- Way two: JavaScript. It moves on as soon as the script runs. -->
<script>location.replace("https://www.example.com/tea/");</script>
  • Line 2 is a meta refresh. The 0 means "after 0 seconds", and URL= gives the new address. The HTML standard counts those seconds from the moment the old page has completely loaded.
  • Line 5 is a JavaScript redirect. location.replace() goes to the new address as soon as the script runs.

Both lines come from the course's old pages. It's like a shop that lets you in, then hands you a note: "Sorry, we're next door." You had to walk in and read the note before you could leave. Here is what the meta refresh cost Asha, next to what the tea page measured:

Two clocks after a meta refreshPop out in the code editor (opens in a new tab)JavaScript
// The median of 3 visits through an old page with a meta refresh, in ms.const movedOn = 315.1; // when the old page sent Asha on, counted from her clickconst ttfb = 108.6; // the tea page's TTFB, on its own clockconst firstPaint = 332; // the tea page's first paint, on its own clock console.log(`The tea page reports a TTFB of ${ttfb} ms.`);console.log(`Asha saw its first paint ${(movedOn + firstPaint).toFixed(1)} ms after her click.`);
  • Line 2 is when the old page sent Asha on: 315.1 ms after her click.
  • Lines 3 and 4 are the tea page's own TTFB and first paint. They count from the moment the tea page's visit began, not from Asha's click.
  • Line 6 prints The tea page reports a TTFB of 108.6 ms.
  • Line 7 adds the two clocks together. It prints Asha saw its first paint 647.1 ms after her click.

On this tiny old page, the meta refresh cost about as much as an HTTP redirect on the same host. Compare Meta refresh and Missing slash in the playground. The real trouble is the measurement. The meta refresh started a brand-new visit, with a clock of its own. So the tea page reports a fast 108.6 ms TTFB, and the first 315.1 ms that Asha spent are in nobody's numbers.

The Navigation Timing standard agrees: it doesn't count a meta refresh as a redirect at all. A JavaScript redirect works the same way, and the exercises ask you to measure one. MDN's advice: "When possible, use HTTP redirects and don't add <meta> element redirects."

Chains from HTTP, www, and trailing slashes

Back to Asha's old link. Why did it need three redirects? Because the address was out of date in three ways, and the shop fixed each one with its own rule:

  • The scheme. The link says http://, but the shop only serves https:// now.
  • The host. The link says example.com, but the shop lives at www.example.com.
  • The trailing slash. The link ends in /tea, but the page is /tea/.

All three are common. MDN calls the www one "a common case". web.dev, the Chrome team's site for web developers, says the other two often cause redirects on a site's own links. Each rule on its own is reasonable. Together they make a redirect chain: a redirect that leads to another redirect.

Here are the four requests from the picture, with the times DevTools recorded:

Asha's old link, request by requestPop out in the code editor (opens in a new tab)JavaScript
// Asha's old link, the median of 3 visits in Chrome: when each request started, in ms, from DevTools.const hops = [  { at: 0, status: 307, url: "http://example.com/tea" },  { at: 1, status: 301, url: "https://example.com/tea" },  { at: 311, status: 301, url: "https://www.example.com/tea" },  { at: 621.9, status: 200, url: "https://www.example.com/tea/" },];const firstByte = 725.3; hops.forEach((hop, i) => {  const end = hops[i + 1]?.at ?? firstByte; // the next request's start, or the page's first byte  console.log(`${hop.status} ${hop.url} took ${(end - hop.at).toFixed(1)} ms`);});
  • Lines 3 to 6 are the four requests: when each one started, its status code, and its address.
  • Line 8 is when the tea page's first byte arrived.
  • Line 11 finds where each request ended: when the next one started, or for the last one, the first byte.
  • Line 12 subtracts the start from the end, and prints how long each request took.

It prints four lines:

  • 307 http://example.com/tea took 1.0 ms
  • 301 https://example.com/tea took 310.0 ms
  • 301 https://www.example.com/tea took 310.9 ms
  • 200 https://www.example.com/tea/ took 103.4 ms

Now take the three costs one at a time.

The http hop never left Chrome

The first request took 1.0 ms, far less than a round trip. It never reached a server. Chrome changed the address itself. The Chromium blog explains: "Chrome will automatically upgrade all http:// navigations to https://, even when you click on a link that explicitly declares http://." If the https:// address fails, Chrome falls back to http://.

DevTools still shows the upgrade as a request: a 307, with the reason HttpsUpgrades. A browser without these upgrades sends the http request to a server instead. Exercise 1 shows what that cost.

A new host needs a new connection

Real-life analogyA shop that moved to another town

If your tea shop moves two doors down, you walk over and ask. If it moves to another town, you first find the town on a map. Then you drive there, and show your card at the door. Only then can you ask for tea. The asking takes the same time as before. Everything in front of it is new.

In real life: Finding the new town on a map
In JavaScript: A DNS lookup for the new host name
In real life: Driving there
In JavaScript: The TCP handshake, which opens a connection
In real life: Showing your card at the door
In JavaScript: The TLS handshake, which makes the connection private
In real life: Asking for the tea
In JavaScript: The request and its answer
In real life: A shop two doors down
In JavaScript: The same host, on the open connection: just the request

The second and third requests went to new host names: example.com, then www.example.com. Over HTTP/1.1, as here, every host name needs a connection of its own. (HTTP/2 can sometimes share one between names; HTTP/1.1 and HTTP/2 showed when.) Here, a new https connection costs three round trips:

  • one for the TCP handshake, which opens the connection;
  • one for the TLS handshake, which makes it private. Here it was TLS 1.3, which needs one round trip, as TLS without the wait showed;
  • one for the request and its answer.

Three round trips of 100 ms: that's the 310.0 and 310.9 ms. On a real network, add a DNS lookup for each new host name too.

The last request went to www.example.com again. The connection from the third request was still open, so it only needed the request: one round trip, 103.4 ms. A trailing-slash redirect costs the same on its own. The course timed one: the Missing slash route added 101.7 ms to TTFB.

Why it matters: every change of host pays for a new connection. Asha's chain changed host twice, so it paid twice. In the Network panel, each request is a row of its own, so a chain is easy to spot.

One rule, one hop

You can't change the links on other people's sites. But you can answer them in one hop, with one rule that fixes everything at once. The course's servers ran this one for the one-hop visits:

One rule: straight to the final URLJavaScript
function redirectOnce(response, url) {  const path = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;  const final = `https://www.example.com${path}${url.search}`;  if (url.href === final) return false;  response.writeHead(301, { location: final, "content-length": 0 }).end();  return true;}
  • Line 2 adds the trailing slash to the path, if it's missing.
  • Line 3 builds the final URL, the address where the page really lives: https, the www host, the path, and any query.
  • Line 4 stops if the request already asked for the final URL. Then the server sends the page itself.
  • Line 5 sends a single 301, straight to the final URL.

Every one of the shop's servers used it. Here is what it did for Asha's old link in Chrome:

A chain, one hop, or no redirectPop out in the code editor (opens in a new tab)JavaScript
// The median TTFB of 3 visits in Chrome, in ms: Asha's old link through a chain or one hop, and a link to the final URL.const ttfb = { "Through a chain of three rules": 725.7, "Through one rule, in one hop": 615.5, "Straight to the final URL": 310.9 }; for (const [route, ms] of Object.entries(ttfb)) console.log(`${route}: ${ms} ms`);
  • Line 2 holds three recorded TTFBs: through the chain, through the one rule, and through a link to the final URL.
  • Line 4 prints Through a chain of three rules: 725.7 ms, Through one rule, in one hop: 615.5 ms and Straight to the final URL: 310.9 ms.

One hop saved 110.2 ms: Chrome went from example.com straight to the tea page. But a link to the final URL saved 414.8 ms, because it never visited the old host at all.

Then came a surprise. Here is the same old link in the Chrome with its upgrades switched off:

The same link, without HTTPS upgradesPop out in the code editor (opens in a new tab)JavaScript
// The same old link in Chrome with its HTTPS upgrades switched off. The median TTFB of 3 visits, in ms.const ttfb = { "Through a chain of three rules": 920.4, "Through one rule, in one hop": 509.3 }; for (const [route, ms] of Object.entries(ttfb)) console.log(`${route}: ${ms} ms`);
  • Line 2 holds the two recorded TTFBs.
  • Line 4 prints Through a chain of three rules: 920.4 ms and Through one rule, in one hop: 509.3 ms.

The chain was slower without upgrades, as you'd expect. But one hop was faster: 509.3 ms, against Chrome's 615.5. Why? Chrome upgraded the link to https://example.com/tea first, so it needed a TLS connection just to read the redirect. Without upgrades, the redirect came over plain http, with no TLS handshake: two round trips instead of three.

Either way, the link to the final URL beat both. This picture shows all five visits:

The median of 3 runs, from DevTools: each request and how long it took, until the final page's first byte. Chain, Chrome: 307 http://example.com/tea 1 ms, then 301 https://example.com/tea 310 ms, then 301 https://www.example.com/tea 310.9 ms, then 200 https://www.example.com/tea/ 103.4 ms; first byte at 725.3 ms. One hop, Chrome: 307 http://example.com/tea 1.1 ms, then 301 https://example.com/tea 307.6 ms, then 200 https://www.example.com/tea/ 306.5 ms; first byte at 615.2 ms. Chain, no upgrades: 301 http://example.com/tea 201.6 ms, then 301 https://example.com/tea 307.2 ms, then 301 https://www.example.com/tea 307.6 ms, then 200 https://www.example.com/tea/ 103.6 ms; first byte at 920 ms. One hop, no upgrades: 301 http://example.com/tea 201.3 ms, then 200 https://www.example.com/tea/ 307.7 ms; first byte at 509 ms. Final URL: 200 https://www.example.com/tea/ 310.4 ms; first byte at 310.4 ms.Chain, Chrome301301200725.3 msOne hop, Chrome301200615.2 msChain, no upgrades301301301200920 msOne hop, no upgrades301200509 msFinal URL200310.4 ms
Each box is one request, and shaded boxes opened a new connection. One hop removes boxes; a link to the final URL needs only one.

Lighthouse, Chrome's page-testing tool, flags chains. Its old audit failed a page "when it has two or more redirects". Since Lighthouse 13, the Document request latency insight does the job, and it flags even one redirect.

web.dev's advice: "Focus on eliminating same-origin redirects, as this is something you have direct control over." A same-origin redirect stays on your own site: the same scheme, host and port.

HSTS and the preload list

Later, Asha follows another old link to the shop: http://www.example.com/cups/. It points at a page she has never opened. This time the host is right. Only the scheme is old. Something has to turn http:// into https://. Who does it: the server, with a redirect, or the browser, by itself?

In a browser without automatic upgrades, the server does it. The browser opens a plain http connection, asks, and gets a 301 to the https address. Then it opens an https connection and asks again. Every old http link costs that first trip, unless the browser already knows better.

Real-life analogyThe shop's card in your wallet

On your first visit, the shop hands you a card: "Always use our front door on Main Road. Valid for one year." You keep it in your wallet. Next time, even if an old flyer shows the side door, you go straight to the front. You never walk to the side door again, for any part of the shop.

In real life: The card the shop hands you
In JavaScript: The Strict-Transport-Security header, sent with an https answer
In real life: "Valid for one year"
In JavaScript: max-age=31536000: one year, in seconds
In real life: Your wallet
In JavaScript: The browser's list of HSTS hosts
In real life: Going straight to the front door
In JavaScript: The browser changing http:// to https:// itself, with no request

That card is HSTS, short for HTTP Strict Transport Security. The server sends it as a header with an https answer: Strict-Transport-Security: max-age=31536000. The max-age is how long to remember it, in seconds. 31,536,000 seconds is one year.

RFC 6797, the standard that defines HSTS, says what the browser does next. Before it loads any http URL on that host, it must change the scheme to https. So no request goes out over http at all. One warning: the browser ignores the header if it arrives over plain http. Send it with your https answers.

The course timed Asha's second visit two ways: after a first visit that sent exactly that header, and after one that didn't. First, in the Chrome without upgrades:

The second visit, without HTTPS upgradesPop out in the code editor (opens in a new tab)JavaScript
// Asha's second visit, through an old http:// link to /cups/, without HTTPS upgrades. The median TTFB of 3 visits, in ms.const withHsts = 316;const withoutHsts = 510.5; console.log(`With HSTS: ${withHsts} ms`);console.log(`Without HSTS: ${withoutHsts} ms`);console.log(`HSTS saved ${(withoutHsts - withHsts).toFixed(1)} ms.`);
  • Lines 2 and 3 are the recorded TTFBs: when the first visit sent HSTS, and when it didn't.
  • Lines 5 and 6 print With HSTS: 316 ms and Without HSTS: 510.5 ms.
  • Line 7 prints HSTS saved 194.5 ms.

That's about two round trips: the plain http connection and its request, which the browser never made. It worked for /cups/, a page the browser had never loaded, because HSTS covers the whole host.

And in Chrome as it ships?

The same second visit, in ChromePop out in the code editor (opens in a new tab)JavaScript
// The same second visit in Chrome as it ships. The median TTFB of 3 visits, in ms.const withHsts = 307.7;const withoutHsts = 312.6; console.log(`With HSTS: ${withHsts} ms`);console.log(`Without HSTS: ${withoutHsts} ms`);
  • Lines 2 and 3 are the recorded TTFBs, with and without HSTS.
  • Lines 5 and 6 print With HSTS: 307.7 ms and Without HSTS: 312.6 ms.

Almost no difference: Chrome's automatic upgrade had already done HSTS's job. DevTools named the reason for each change, HSTS or HttpsUpgrades. Both happened inside the browser, in a few milliseconds.

Median TTFB of 3 repeat visits through an old http:// link to another page. Chrome, HSTS: 307.7 ms, the http URL upgraded inside Chrome (HSTS). Chrome, no HSTS: 312.6 ms, the http URL upgraded inside Chrome (HttpsUpgrades). No upgrades, HSTS: 316 ms, the http URL upgraded inside Chrome (HSTS). No upgrades, no HSTS: 510.5 ms, the http URL answered by the server with a 301.Chrome, HSTS307.7 msChrome, no HSTS312.6 msNo upgrades, HSTS316 msNo upgrades, no HSTS510.5 ms
Shaded: Asha's first visit got HSTS. It only made a difference in the browser without automatic upgrades.

Why it matters: hstspreload.org says Chrome and Safari upgrade http:// navigations by themselves. Browsers that don't still pay for the http trip on every old link, until your site sends HSTS. So send it anyway.

The preload list

HSTS has one gap: the very first visit. The browser can't know about the card before the shop hands it over. The HSTS preload list closes that gap. It's a list of sites built into the browser itself, so even a first visit goes straight to https. Sites apply to join at hstspreload.org, which sets three rules for the header:

  • The max-age must be at least 31536000 seconds, one year.
  • It must include includeSubDomains, so HSTS covers every subdomain too.
  • It must include preload.

For speed, though, preloading adds little now, because Chrome and Safari upgrade every http:// navigation, first visits included. hstspreload.org puts it plainly: "While HSTS is recommended, HSTS preloading is not recommended."

It also warns that preloading covers every subdomain, even internal ones. And it suggests turning HSTS on in stages, starting with a short max-age. The exercises ask where it starts.

Marketing and tracking redirects

The Mint tea shop sends a newsletter. Asha clicks "See the new teas" in the email. She doesn't go straight to the shop. The link goes first to click.example.net, the email service's tracker. A tracker is a server that counts the click, then redirects the visitor on, here with a 302. Ads and link shorteners work the same way.

web.dev warns that this hurts most on sites with many visitors from ads or newsletters, because those links often go through a measuring service first.

Real-life analogyA survey desk at the mall entrance

A flyer invites you to a tea shop in the mall. At the mall's entrance, a survey desk stops you: "How did you hear about us?" You tick a box, and they point you on. If the flyer printed the shop's old address, you then follow the old signs as well. The fix is on the flyer: print the address where the shop is now.

In real life: The flyer in your letterbox
In JavaScript: The link in the newsletter
In real life: The survey desk at the entrance
In JavaScript: The tracker, on a host of its own
In real life: Ticking "I came from the flyer"
In JavaScript: Counting the click
In real life: Being pointed on to the shop
In JavaScript: The tracker's 302 to the shop
In real life: An old address printed on the flyer
In JavaScript: Giving the tracker an old URL, so more redirects follow it

The tracker is another host, so it needs a new connection. In the playground, pick Newsletter tracker: two requests, each on a new connection. Asha's TTFB was 611 ms, against 310.9 ms for a direct link. Then try Shortener, then tracker and Tracker, old URL.

Now look at what the tea page itself could see about that visit:

What the tea page sawPop out in the code editor (opens in a new tab)JavaScript
// What the tea page measured, in the median of 3 visits through the newsletter's tracker, in ms.const navigation = { redirectCount: 0, redirectStart: 0, fetchStart: 305.8 };const ttfb = { value: 611, waitingDuration: 305.8 }; console.log(`The page counted ${navigation.redirectCount} redirects.`);console.log(`Its TTFB was ${ttfb.value} ms, and ${ttfb.waitingDuration} ms of it came before the page's own request.`);
  • Line 2 is from the tea page's navigation entry, its list of times from The navigation timeline.
  • Line 3 is the TTFB that web-vitals measured on the page, with its waitingDuration: the time before the browser started fetching the page.
  • Line 5 prints The page counted 0 redirects.
  • Line 6 prints Its TTFB was 611 ms, and 305.8 ms of it came before the page's own request.

The page counted no redirects, yet half its TTFB went on one. The Navigation Timing standard explains the 0. It says redirectCount must return zero "if there is any redirect that is not from the same origin as the destination document". The tracker is on another origin, so the count is hidden. So are redirectStart and redirectEnd.

When every hop stays on the shop's own origin, the count works. The missing-slash visit counted 1.

TTFB still counts from Asha's click, so the tracker's time is inside it. web-vitals puts it in waitingDuration, the part Time to first byte introduced. Why it matters: in field data, a large waiting part on visits from a campaign is the clue. Split your visits by where they came from, as Segmenting field data split them by device and page.

You can't remove the tracker: it's how the newsletter counts clicks. But you can stop it adding more hops. web.dev: "Ensure the URL provided to advertisers or newsletters is the correct final URL, so as not to add another redirect to the ones used by those services."

Fix it where?

Each redirect below has a best fix. Some you can remove by changing your own links. Some you can only shorten. Some need HSTS. And some do a job worth keeping. Sort each one:

Fix it where?
  • Your own menu links to /tea, which redirects to /tea/
  • The newsletter's tracker is given https://example.com/tea
  • Old http://example.com links on other sites go through three redirects
  • The old domain redirects to the new domain, which redirects to www
  • Visitors in browsers that don't upgrade http:// follow old http links to your site
  • Every subdomain should be reached over HTTPS only
  • Bookmarks to pages from before a redesign
  • After a form POST, a 303 to the order page
Try it yourself
0 of 8 correct

Sort each redirect by its fix, or keep it.

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

Common mix-ups

  • "Every redirect costs one round trip." No. On an open connection, yes: the missing slash added 101.7 ms. To a new host, it also pays for a new connection. Asha's two host hops took 310.0 and 310.9 ms.
  • "301 and 302 only differ for search engines." No. Chrome kept the 301 in its cache. On the repeat visit, it took 305.8 ms, against 408.4 for the 302.
  • "redirectCount shows every redirect." No. After the tracker, it said 0. Any redirect from another origin hides the count.
  • "The final page's TTFB is what the visitor waited." No. After a meta refresh, the tea page reported 108.6 ms. Asha saw its first paint 647.1 ms after her click.
  • "HSTS preloading is needed for speed." No. hstspreload.org recommends HSTS, but not preloading. Chrome and Safari upgrade http:// by themselves.
  • "One hop is as good as none." No. One hop took 615.5 ms in Chrome. A link to the final URL took 310.9.

Practice exercises

Try each one before you open the hints. Exercises 1 and 2 are code. Exercises 3 and 4 use the playground in the first section. Exercises 5 and 6 send you to the standards.

Exercise 1 · Warm-upThe http hop, without upgrades

Without HTTPS upgrades, the first request reached a server. What does the first line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// Asha's old link, the median of 3 visits in Chrome with its HTTPS upgrades switched off: when each request started, in ms, from DevTools.
const hops = [
  { at: 0, status: 301, url: "http://example.com/tea" },
  { at: 201.6, status: 301, url: "https://example.com/tea" },
  { at: 508.8, status: 301, url: "https://www.example.com/tea" },
  { at: 816.4, status: 200, url: "https://www.example.com/tea/" },
];
const firstByte = 920;

hops.forEach((hop, i) => {
  const end = hops[i + 1]?.at ?? firstByte; // the next request's start, or the page's first byte
  console.log(`${hop.status} ${hop.url} took ${(end - hop.at).toFixed(1)} ms`);
});

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

    Exercise 2 · Warm-upA short link, then the tracker

    Asha clicks a short link that leads to the tracker, then to the shop. What does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // What the tea page measured, in the median of 3 visits through a short link, then the newsletter's tracker, in ms.
    const navigation = { redirectCount: 0, redirectStart: 0, fetchStart: 613.2 };
    const ttfb = { value: 921.7, waitingDuration: 613.2 };
    
    console.log(`The page counted ${navigation.redirectCount} redirects.`);
    console.log(`Its TTFB was ${ttfb.value} ms, and ${ttfb.waitingDuration} ms of it came before the page's own request.`);

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

      Exercise 3 · PracticeA tracker given an old URL

      The newsletter gave its tracker an old URL. How long after Asha's click did the tea page's first byte arrive?

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

        Exercise 4 · PracticeA JavaScript redirect

        Through the old page with a JavaScript redirect, how long after Asha's click was the first paint?

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

          Exercise 5 · ChallengeHow many redirects?

          A badly set-up site can send the browser round in a loop. How many redirects will a browser follow for one request before the fetch fails?

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

            Exercise 6 · ChallengeHSTS in stages

            hstspreload.org suggests turning HSTS on in stages, with a longer max-age each time. What max-age does the first stage use, in seconds?

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

              Check your understanding

              For each question, count the requests, ask which ones opened a new connection, and ask what the final page could see.

              Redirects quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does the second line of the repeat-visit example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The median of 3 repeat visits to /old-tea, after every connection had closed: TTFB in ms, and where the redirect came from.
                const repeatVisit = {
                  301: { ttfb: 305.8, fromCache: true },
                  302: { ttfb: 408.4, fromCache: false },
                  307: { ttfb: 408.9, fromCache: false },
                  308: { ttfb: 305.6, fromCache: true },
                };
                
                for (const [status, { ttfb, fromCache }] of Object.entries(repeatVisit)) {
                  console.log(`${status}: ${ttfb} ms, ${fromCache ? "the redirect came from Chrome's cache" : "the server was asked again"}`);
                }

                Choose an answer to see the explanation.

              2. Question 2 of 8What does the last line of the HSTS example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // Asha's second visit, through an old http:// link to /cups/, without HTTPS upgrades. The median TTFB of 3 visits, in ms.
                const withHsts = 316;
                const withoutHsts = 510.5;
                
                console.log(`With HSTS: ${withHsts} ms`);
                console.log(`Without HSTS: ${withoutHsts} ms`);
                console.log(`HSTS saved ${(withoutHsts - withHsts).toFixed(1)} ms.`);

                Choose an answer to see the explanation.

              3. Question 3 of 8Asha's old link went to example.com, then to www.example.com. Why did each of those two hops cost about three round trips, when the missing slash cost one?

                Choose an answer to see the explanation.

              4. Question 4 of 8In Chrome, Asha's old link took longer in one hop than it did in a browser without HTTPS upgrades. Why?

                Choose an answer to see the explanation.

              5. Question 5 of 8An old page sends visitors on with a meta refresh. Why does the final page's field TTFB look fast?

                Choose an answer to see the explanation.

              6. Question 6 of 8A visitor arrives through a newsletter's tracker on another site. What does the final page's redirectCount say?

                Choose an answer to see the explanation.

              7. Question 7 of 8A page has moved for good, and forms on other sites still POST to its old address. Which status keeps their POST working?

                Choose an answer to see the explanation.

              8. Question 8 of 8What does hstspreload.org advise about HSTS and its preload list?

                Choose an answer to see the explanation.

              Key takeaways

              • A redirect is a whole extra request before the page. On an open connection, it costs one round trip.
              • A redirect to a new host or scheme also costs a new connection: three round trips here, plus DNS on a real network.
              • Link to the final URL, in your own pages and in what you give newsletters and advertisers.
              • When you can't change a link, answer it with one redirect, straight to the final URL.
              • Use 301 or 308 only for moves that are for good: browsers keep them. 307 and 308 keep a form's POST.
              • Send HSTS with your https answers. Don't preload just for speed.
              • Meta refreshes and JavaScript redirects restart the page's clock, and redirects through other origins aren't counted. In field data, look at waitingDuration.

              Remember the one-liner.
              Every redirect is another trip. Link to the final URL; when you can't, get there in one hop.

              Sources

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

              Coming next: 103 Early Hints, also in Fast servers, caches, and networks. While the server is still building the page, it can tell the browser which files to start fetching.

              CompleteFrontend Clear concepts. Working examples.