Debugging the Network
Read the waterfall for its shape, not its totals. Then learn the three ways the network panel lies: the CORS error that is a 500, the cache row that is not a hit, and the request that is missing because a service worker answered it.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What did the browser actually request, in what order, what was each request waiting for, and where did the response really come from?
A person opened a page and is looking at a spinner, an empty list, or an error message that does not describe anything they did. They want their data.
Open the Network panel, sort by time, and look at the slowest request. That is the bottleneck, so make it faster.
The slowest request is often not on the critical path. A large image that finishes last while content is already interactive costs nobody anything; a tiny stylesheet discovered late blocks the first paint entirely (Render-Blocking Resources).
- The slowest request is often not on the critical path. A large image that finishes last while content is already interactive costs nobody anything; a tiny stylesheet discovered late blocks the first paint entirely (Render-Blocking Resources).
- Duration hides what the request was doing. Queued behind connection limits, blocked on a preflight, stalled waiting for a connection, waiting on the server, or downloading a large body are five different problems with the same total (Reading a Network Waterfall).
- The characteristic loading problem is not one slow request but a staircase: each request only discoverable after the previous one is parsed, so latency multiplies by the depth of the chain rather than adding up.
- The console says CORS and the actual problem is a 500. An error response that fails before your framework attaches its headers has no
Access-Control-Allow-Origin, so the browser refuses the response and reports the refusal, not the status (CORS). - Some requests are missing from the panel entirely, or shown in a way that does not distinguish them from network trips, because a service worker answered them from a cache (Intercepting Fetch).
What is actually happening
In the browser, not in the framework.
- The waterfall is a picture of discovery and dependency, not of size. A request starts when the browser learns it needs the resource — from the parser, the preload scanner, a stylesheet it has just parsed, or a script it has just run (The Preload Scanner).
- Each row decomposes into phases the browser distinguishes: queueing and priority, connection setup (DNS, TCP or QUIC, TLS), request sent, waiting for the first byte, and content download. Which phase dominates points at a completely different owner (The Three-Way Handshake in Networking).
- A response can come from several places before the network: a service worker's
fetchhandler, the in-memory cache, the HTTP disk cache, or a conditional revalidation that returns 304 and reuses the stored body (Browser HTTP Caching). - CORS is a browser policy about whether *your script* may read a cross-origin response. The request usually happened; the response usually arrived; the browser withheld it from your code. Server-side authorization is a separate matter that the browser knows nothing about (CORS).
- A preflight is a separate
OPTIONSrequest that must succeed before the real one is sent. When it fails, the row you are looking for was never sent at all, and the panel shows anOPTIONSyou did not write. - The browser prioritises: render-blocking resources ahead of images, and a limited number of connections per origin on older protocol versions, which produces queueing that looks like server slowness (HTTP/2: Streams on One Connection in Networking).
What this makes the browser do
And which of it is avoidable.
- Connection setup for each new origin: a DNS lookup, a connection, and a TLS handshake before a single byte of your resource moves. Every third-party origin on the critical path pays this again (Resource Hints).
- Parsing responses: JSON parsing of a large payload is main-thread work charged to your page, and it does not appear in the network row at all (The Real Cost of JavaScript).
- Decompression and, for images, decode — also real work, also invisible in a duration column (Images and Fonts).
- Revalidation: a conditional request still costs a round trip even when the answer is "unchanged", which is why a page full of 304s can be slow while looking perfectly cached.
The shape is the finding
Every loading investigation should begin by looking at the picture from a distance before reading a single row. Three shapes cover most of what you will find. A staircase means each request could only be discovered after the previous one was fetched and parsed, so the page is paying serialized latency; that is a structural problem, and making any individual request faster barely helps. A wall means everything started at once and then queued, which is a priority or connection-limit problem. A long empty gap at the start means the document itself was late, and nothing on the client can recover it (The Critical Rendering Path).
The staircase below is the classic one, and every step of it is a real dependency: the HTML must be parsed to discover the stylesheet, the stylesheet must be parsed to discover the font it references, and the data request cannot be issued until the script that issues it has been downloaded, parsed and run. The fix is to break the chain — hint the font, inline the critical style, issue the data request from the document — not to shave bytes off each link (Resource Hints).
- Document request — Nothing else can be discovered until bytes of HTML arrive.
- HTML parse (streaming) — Discovery begins mid-response, which is why streaming matters (Streaming HTML).
- Stylesheet fetch — Discovered by the parser; render-blocking from this point.
- Stylesheet parse — The font URL only exists after this.
- Font fetch — Third link in the chain, and often on a third origin.
- Application script fetch — Parallel with the stylesheet, but the data call waits on it.
- Script parse + execute — Main-thread work, not a network cost (The Real Cost of JavaScript).
- Data request — Fourth round trip deep. This is the staircase.
Read the depth of the chain, not the length of any bar. Four serialized round trips on a high-latency connection is the whole problem, and no single request in this picture is anomalous.
The three ways the panel lies
The network panel is unusually honest about what it records and unusually easy to over-read. The failures below are not exotic: between them they account for a large share of the hours frontend engineers lose to this layer, and each has a check that takes seconds once you know to make it.
The CORS row deserves special attention because the message is genuinely misleading. The browser is telling you that it refused to hand your script a response it does not have permission to expose. That is compatible with the server having returned a 500, a redirect to a login page, or a 404 from a URL with a typo — none of which are header configuration problems, and all of which get "fixed" by changing headers that were never the cause (CORS).
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Cross-origin request to an endpoint that threw | Console reports a CORS failure; no useful status anywhere | The error response was produced before the middleware that adds the access-control headers, so the browser withheld it from your script | Read the status in the network row, or reproduce with a non-browser client where CORS does not apply. Fix the 500; check headers afterwards (Network Failures Only the Client Can See). |
| A cross-origin request with a custom header or an unusual method | The request you wrote never appears; an OPTIONS appears instead | The preflight failed, so the real request was never sent | Debug the OPTIONS response on its own terms — it is a separate request with its own status and headers (CORS). |
| A request answered by a service worker | A row that looks like a network trip but has no server-side trace, or no row at all | The worker's fetch handler responded from Cache Storage, or synthesised a response entirely | Check the worker's state and scope in the Application panel, unregister it, and reload to compare (Intercepting Fetch). |
| Repeat navigation with a cached bundle | Every asset shows a 304 and the page still feels slow | Revalidation is a full round trip per resource; a cache that must always ask is barely a cache | Use content-hashed URLs with long-lived caching so the answer is "reuse it" without a request (Content-Hashed Assets). |
| An endpoint returning an error object with a success status | The UI renders empty rather than showing an error | The client branches on status only, so the error body is parsed as data | Validate the response shape, not just the status, and give the failure a real UI state (The Life of a Fetch). |
| User has a content blocker or corporate proxy | A request fails only for some users, with a network-level error and no server log | Something between the page and the origin refused or rewrote the request | Reproduce with the extension enabled; degrade gracefully when a non-essential request fails (Third-Party Scripts and the Supply Chain). |
| A navigation away while requests are in flight | Errors in tracking that no user ever reports seeing | Cancelled requests reported as failures | Distinguish abort from failure at the call site and stop counting aborts as errors (Cancelling a Request Nobody Is Waiting For). |
Where the response actually came from
A fetch in your code is not a network request. It is a request into a stack of things that may answer it, and the difference between them explains both stale data and requests you cannot find. The order below is the one to walk when a response is wrong, missing, or older than it should be.
The state that catches people is the middle of the table: revalidated. A resource that is cached but must be checked still costs a round trip. It looks like a cache hit in casual reading and behaves like a network request on a high-latency connection, and it is why "we added caching" and "it got faster" are separate claims that both need evidence (Browser HTTP Caching).
- 1Your data layer
Returns a cached entry or joins an in-flight request for the same key, without calling
fetchat all (Five Components, One Request).fails by Producing no network row, so a stale value looks like a server problem when the request never happened.
- 2Service worker
Runs a
fetchhandler that may serve from Cache Storage, synthesise a response, or pass through to the network (Intercepting Fetch).fails by Serving a previous deployment's assets to a page from the current one, producing errors that match neither version.
- 3Memory cache
Reuses a resource already fetched by this document, within its lifetime.
fails by Hiding a caching-header mistake for the length of a session, so the bug only appears after a reload.
- 4HTTP disk cache
Reuses a stored response if it is still fresh, according to the response's own caching headers.
fails by Serving a long-lived response from a URL that was supposed to change, which is what content hashing exists to prevent (Content-Hashed Assets).
- 5Revalidation
Sends a conditional request and gets a 304, reusing the stored body without transferring it again.
fails by Being read as a cache hit. The body was not transferred; the round trip still was.
- 6Network
Resolves, connects, sends, waits for the server, downloads.
fails by Being blamed for everything above it, because it is the only step with a visible duration.
When something is stale, walk this list top down and ask which layer answered. When something is missing from the panel, walk it top down and ask which layer answered without asking.
| State | What it cost | How you tell | What it explains |
|---|---|---|---|
| Fresh from the network | Full round trip plus transfer | A normal row with all phases and a real transfer size | Nothing stale; latency is the whole story |
| Revalidated (304) | A round trip; no body transfer | Small transfer size, a 304 status, and a conditional request header | "We are cached and still slow" — usually the answer |
| From the HTTP cache | Decode and parse only | The response-source column says cache; no request headers were sent | Stale assets after a deploy without content hashing (Deploying a Frontend) |
| From a service worker | Whatever the handler did | The row is marked as service-worker-served; the Application panel shows a controlling worker | Version skew and offline behaviour (Caching Strategies) |
| Never requested | Nothing | No row at all; your data layer returned an entry | Stale UI that no amount of server debugging will explain (Query Keys and Invalidation) |
How to build it
Most important first.
- Read the waterfall for shape first. A staircase means serialized discovery; a wall of rows starting together means a connection or priority limit; a long flat gap before anything means the document itself was late (The Critical Rendering Path).
- Check what happened before the first row, not only the rows. Redirects, a slow first byte and a late-discovered stylesheet all live at the left edge of the picture.
- Read the response-source column on every row you reason about. "From cache", "revalidated with 304", "served by a service worker" and "fetched fresh" are four different states and only one of them is a network trip (Stale-While-Revalidate).
- When the console says CORS, look at the status of the request in the network panel and, if you can, at the server's log. Fix the 500 first; the header problem may not exist (Network Failures Only the Client Can See).
- Reproduce first visits honestly: clear storage, unregister the service worker, and reload — a hard reload alone does not settle whether a service worker is controlling the page (The Service Worker Lifecycle).
- Check the request as sent, not as written: headers the browser added, cookies attached automatically, the resolved URL after redirects, and whether credentials were included at all (Cross-Site Request Forgery).
- When a request is missing, ask who could have answered it: a service worker, the HTTP cache, an extension, a client-side cache in your data layer that never made the call (Five Components, One Request).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Network failure is a user-facing state, not a console message. If a request fails and only the console knows, a screen-reader user experiences a page that stopped changing for no stated reason (Loading, Error, Empty — The States You Did Not Render).
- Announce the outcome, not the transition. A retry that succeeds should say so once; a spinner replaced by content with no announcement leaves someone waiting on a page that is already finished (Live Regions and Announcement).
- Keep focus somewhere sensible while a request is in flight. Disabling the control the user just activated without moving focus deliberately drops them out of the flow (Focus Management).
- When debugging a report of "it never loads", check whether the error surface is reachable at all by keyboard — a retry button inside a toast that disappears on a timer is unreachable for anyone who does not move fast enough (Keyboard Operability).
What can go wrong
- A CORS message that leads to a day of header configuration for a bug that was a server exception, a wrong URL producing a 404 page, or a redirect to a login page.
- A "cached" page that revalidates every resource on every navigation, paying a full round trip per resource to be told nothing changed.
- A stale service worker serving an old bundle to a client whose HTML is new, producing errors that make no sense against either version (Deploying a Frontend).
- A request cancelled by a navigation or an
AbortControllerand read as a server failure. Cancelled is a different outcome from failed and needs to be handled as one (Cancelling a Request Nobody Is Waiting For). - A 200 carrying an error body. Reading only the status code means your success path parses an error object and renders nothing (The Life of a Fetch).
- A request blocked by an extension, a content blocker or a corporate proxy — reproducible for the user, invisible in your environment, and often reported as a client-side bug (Third-Party Scripts and the Supply Chain).
- Throttling used as proof. It changes bandwidth and latency in a model; it does not reproduce packet loss, radio wake-up, or a proxy that buffers.
- Two requests for the same resource issued by different components can return in either order, and the later-arriving response can overwrite the newer one (Out-of-Order Responses).
- A service worker installing during the first visit may or may not control the page for the requests that page makes; the same load produces different waterfalls depending on which won (The Service Worker Lifecycle).
- A preflight and the request it authorises are separate trips, so a cached preflight makes the second attempt behave differently from the first.
- A navigation cancels in-flight requests, so a bug reported as "the request failed" is sometimes "the user moved on and the browser cancelled it" (Cancelling a Request Nobody Is Waiting For).
- Retries can race the original: the first attempt succeeds late, the retry succeeds first, and both effects land (Retries, and the Duplicate Order).
- A CORS error means the browser refused to give your script a cross-origin response. It is not a statement about who may call the endpoint or what the server allows; a request that your script cannot read may still have been executed (The Same-Origin Policy).
- Whether cookies were attached is part of the reproduction. Credentials mode,
SameSitebehaviour on a cross-site request, and a third-party context that no longer receives cookies at all are common causes of "works for me, 401 for them" (Cookies). - HAR exports contain everything: bodies, cookies, tokens. They are the most casually shared secret in frontend engineering (Session Replay and the Privacy It Costs).
- Local request overriding and response mocking prove things about your client and nothing about the server. Any rule that matters must be enforced where the user cannot edit it (What the Frontend Is Responsible For in Auth).
- "The console says CORS, so it is a CORS problem." Very often the response is a 500 or a redirect that simply arrived without the headers the browser required in order to hand it to your code (CORS).
- "It says 200, so it worked." Status describes the transfer. The body can be an error object, an HTML login page, or an empty array where the data used to be.
- "There is no request, so the code did not run." A service worker, an HTTP cache hit, or a deduplicating data layer can all satisfy a call without a visible network row (The Client Cache Model).
- "Cached means free." A revalidation is a round trip; a cache hit still costs decode and parse; and a cache that stores the wrong thing is worse than no cache at all (Browser HTTP Caching).
- "The request is slow, so the server is slow." Waiting for the first byte points at the server. Queueing, connection setup and download do not (Why Is My API Slow? in Backend).
Measuring it, and what changes in the field
- The Network panel waterfall, read as shape: what started when, what blocked, what was still open at first paint (Reading a Network Waterfall).
- The response-source column and the request headers, to separate fresh, revalidated, cached and service-worker-answered responses.
- Field data for failure rates by endpoint and by client, because the failures that matter are the ones you cannot reproduce (Network Failures Only the Client Can See).
- Server-side logs and traces for the same request id, which is the only way to tell a client-visible error from a server-side one (Correlation IDs: Turning Lines Into a Story in Observability).
- On a high-latency network, a staircase becomes the dominant cost: each additional serialized round trip is paid in full, and total bytes stop being the interesting number (Bandwidth vs Latency in Networking).
- On a flaky connection, retries and duplicate deliveries appear, and any endpoint that is not idempotent starts producing duplicate effects (Retries, and the Duplicate Order).
- On an older protocol version or through a proxy that downgrades, per-origin connection limits reappear and requests queue in a way that looks like server slowness (HTTP/1.1 vs HTTP/2 vs HTTP/3 in Networking).
- On a repeat visit, most of the waterfall is cache decisions rather than transfers, and a first-visit investigation cannot be done from that state (Content-Hashed Assets).
- In an installed or offline-capable app, the service worker is between you and every row, and its version is part of the reproduction (Caching Strategies).
- Reading the waterfall properly is slower than sorting by duration, and it is the only reading that finds the staircase, which is the more common problem.
- Reproducing with a cleared service worker and an empty cache tells you about first visits and hides every bug that only affects returning users. Both states have to be debugged, separately.
- Throttling makes network-shaped bugs reproducible and changes timing enough to hide races that occur at full speed.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- BROWSER-SPECIFICHow request phases are broken down and labelled differs: Chromium, Firefox and Safari split queueing, stalling, connection setup and time-to-first-byte differently, and the way a service-worker-answered response is marked is not the same in any two of them. The phases exist everywhere; the columns do not.
- NETWORK-SPECIFICProtocol version changes the shape of the waterfall: per-origin connection limits and head-of-line blocking behave differently on HTTP/1.1, HTTP/2 and HTTP/3, so the same page produces a different picture over a proxy that downgrades the connection (Head-of-Line Blocking in Networking).
- GENERALThe discovery-and-dependency reading of a waterfall, and the separation of fresh, revalidated, cached and worker-answered responses, follow from the fetch and HTTP caching specifications rather than from any one browser.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — contract tests and fault injection at the client boundary, so that a 500, a timeout and a cancellation are all exercised rather than discovered in production.