Monitoring Single-Page Apps: When 200 OK Means Nothing
SPAs return 200 with an empty shell even when a JavaScript error blanks the page. Why classic uptime checks miss SPA failures, and what catches them.
Want this checked continuously?
CompleteStatus grades your headers, SSL and email security 24/7 — free, commercial use allowed.
Single-page applications are everywhere now. React, Vue and Angular power a growing share of the web, and the architecture has real advantages: fluid navigation, clean API separation, app-like UX. But in the move from server-rendered pages to client-rendered apps, something went mostly unnoticed: uptime monitoring stopped meaning what most people think it means.
A classic HTTP check asks your server for a page and is satisfied by 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 can do that perfectly while your users look at a blank white page. The server is up and the application is down. Your monitor sees the first fact and misses the second.
The 200-with-a-blank-page problem
Look at what an SPA's server 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 built in the browser after the bundle loads and runs. So the page can fail in ways the server never sees:
- A runtime JavaScript error during boot. One
TypeError: undefined is not a functionin an early component and rendering stops at an empty#root. The server logged a 200. - The bundle 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, and the page is dead. - A third-party script hangs the boot sequence. An analytics or tag-manager snippet blocks the critical path.
- The API behind the app is down. The shell renders, the spinner spins and nothing arrives. Every HTTP-level signal is green.
In each case a status-code check reports perfect uptime while every user is locked out.
Client-side routing breaks assumptions too
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:
- Monitoring more URLs doesn't tell you more. Checking
/pricing,/dashboardand/settingswith a plain HTTP monitor checks the sameindex.htmlthree times. Three green checks, one fact. - Real 404s need deliberate handling.
https://app.example.com/tihs-does-not-existreturns200 OKwith the shell, and the router shows a "not found" screen client-side: a soft 404. Search engines index these, monitors can't tell them apart, and users bookmark them. If a page doesn't exist, return a real 404 status from the server where you can, or at least render a distinct marker your tooling can detect.
What works: assert on rendered output
Stop asking whether the server responded and start asking whether the application produced 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 passes on a healthy page and fails on an empty
#root. The catch is that the keyword must be checked 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 (the keyword only exists after render) or always passes (the keyword is in the static shell and proves nothing). - Browser-based checks. The best option for SPAs: a real browser engine (headless Chrome has made this practical) loads the page, runs the JavaScript, waits for render and then evaluates assertions. It's the only kind of check that fails when your bundle throws on line one, the same way a user's browser does.
- A render-time budget. A browser check can also require meaningful content within N seconds. An SPA that 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. They fail independently, so monitor them 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, and 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 users who are already logged in and is unusable for everyone else.
- API response time is an early warning. Frontend symptoms like endless spinners usually trail API degradation by minutes.
When the browser check and the API check disagree, you already know which half of the stack to look at before opening a terminal.
Don't forget the delivery path
Your bundle probably lives on a CDN, which adds its own failure modes, and deploys are the riskiest 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. Every new visitor gets a blank page while everyone with a warm cache, including you, sees nothing wrong.
A cheap guard: monitor the bundle URL your current index.html references and alert on anything other than a fast 200. It takes a minute to set up and catches a whole category of "the deploy looked fine" outages.
Monitoring the app your users actually see
SPAs moved the application into the browser, so monitoring has to follow it there. CompleteStatus provides HTTP checks with keyword and response-body assertions for your APIs and content, response-time tracking to catch gradual slowdowns before they become outages, and alerts on several channels when an assertion fails, all in one dashboard. Create a free account and check both the page and the API behind it.
Written with AI assistance and reviewed by the CompleteStatus team.
Get notified when CompleteStatus opens
New accounts are closed while we're in private beta. Leave your email and we'll send one message the moment sign-ups open — nothing else.