Monitoring Single-Page Apps — When 200 OK Means Nothing
Single-page applications have won. React, Vue and Angular power an ever-growing share of the web, and the architecture has real virtues — fluid navigation, clean API separation, app-like UX. But somewhere in the migration from server-rendered pages to client-rendered apps, a quiet casualty went mostly unnoticed: your uptime monitoring stopped meaning what you think it means.
Here's the uncomfortable version. A classic HTTP check asks your server for a page and celebrates a 200 OK. In an SPA, the server's job is to hand over a nearly empty HTML shell and a link to a JavaScript bundle — and it will do that job flawlessly while your users stare at a blank white page. The server is up. The application is down. Your monitor sees the first fact and misses the second entirely.
The 200-with-a-blank-page problem
Look at what an SPA's server actually returns:
curl -s https://app.example.com/dashboard
<!-- roughly what comes back -->
<div id="root"></div>
<script src="/static/js/main.8f3a2c.js"></script>
That's the whole document. Everything users see — navigation, content, the login form — is constructed in the browser after the bundle loads and executes. Which means the page can die in ways the server never witnesses:
- A runtime JavaScript error during boot — one
TypeError: undefined is not a functionin an early component, and rendering stops at a blank#root. The server logged a 200. - The bundle itself fails to load — a bad deploy left
index.htmlpointing at a hashed filename that no longer exists on the CDN. The shell is a 200; the script is a 404; the page is a corpse. - A third-party script hangs the boot sequence — an analytics or tag-manager snippet that blocks the critical path.
- The API behind the app is down — the shell renders, the spinner spins, and nothing ever arrives. Every HTTP-level signal is green.
In each case a simple status-code check reports perfect uptime while every single user is locked out. That's not a monitoring blind spot — it's a monitoring inversion.
Client-side routing breaks assumptions too
The second surprise: in an SPA, every path returns the same document. The router lives in the browser, so the server is usually configured to serve the shell for any URL:
location / {
try_files $uri $uri/ /index.html;
}
Two consequences worth internalizing:
- You can't monitor pages by URL and learn anything new. Checking
/pricing,/dashboardand/settingsagainst a plain HTTP monitor is checking the sameindex.htmlthree times. Three green checks, one fact. - Real 404s need deliberate care.
https://app.example.com/tihs-does-not-existhappily returns200 OKwith the shell, and the router shows a "not found" screen client-side — a soft 404. Search engines index these; monitors can't distinguish them; users bookmark them. If a page genuinely doesn't exist, the ideal is a real 404 status — from the server where possible, or at minimum a distinct rendered marker your tooling can detect.
What actually works: assert on rendered output
The fix follows directly from the failure mode: stop asking "did the server respond?" and start asking "did the application produce what users see?"
- Keyword assertions — on the rendered page, not the shell. A content check that requires, say, your product name in the navigation or a known footer string will pass on a healthy page and fail on a blank
#root. One caveat that trips people up: the keyword must be asserted against the rendered DOM. Fetching raw HTML with curl gets you the empty shell whether the app works or not — so a naive keyword check either always fails (keyword only exists post-render) or always passes (keyword sits in the static shell, proving nothing). - Browser-based checks. The gold standard for SPAs: a real browser engine — headless Chrome has made this practical — loads the page, executes the JavaScript, waits for render, and then evaluates assertions. This is the only kind of check that fails when your bundle throws on line one, exactly like a user's browser does.
- A render-time budget. A browser check can also assert "meaningful content within N seconds." An SPA that technically renders after 30 seconds of loading is, to any human, down.
Monitor the APIs underneath — separately
An SPA is really two systems: a static frontend and the APIs it calls. Monitor them independently, because they fail independently:
- Direct API checks hit your real endpoints —
GET /api/health, or better, a read endpoint that touches the database — and assert on status and response body. An API returning200with{"error": "connection pool exhausted"}should count as down; a JSON assertion catches what a status code can't. - Auth flows deserve their own check. If the token endpoint is broken, the app "works" for logged-in users and is bricked for everyone else.
- Response-time on APIs is an early warning. Frontend symptoms (eternal spinners) usually trail API degradation by minutes. The API monitor is where you see it first.
The payoff is diagnostic speed: when the browser check and the API check disagree, you already know which half of the stack to look at before you've opened a terminal.
Don't forget the delivery path
Your bundle probably lives on a CDN, which adds failure modes of its own — and a deploy is the most dangerous moment. The classic incident: HTML is served from the origin, assets from the CDN, and a deploy or cache purge leaves them referencing different builds. Old shell, missing new bundle — every fresh visitor gets a blank page while everyone with a warm cache sees nothing wrong, including you.
A cheap, high-value guard: monitor the bundle URL your current index.html references and alert on anything but a fast 200. It's a thirty-second setup that catches an entire category of "the deploy looked fine" outages.
Monitoring the app your users actually experience
The theme across all of this: SPAs moved the application into the browser, so monitoring has to follow it there. CompleteStatus was built with that in mind — HTTP checks with keyword and response-body assertions for your APIs and rendered content, response-time tracking to catch the slow fade before the outage, and multi-channel alerts the moment an assertion fails, all in one dashboard. Create a free account and put a check on the rendered page and the API behind it — because in a single-page app, 200 OK is where the questions start, not where they end.