THE REPLACEQA BLOG
Synthetic user journey monitoring vs uptime monitoring
Compare synthetic user journey monitoring and uptime checks to understand what each verifies and how a healthy server can hide a broken task.
An uptime check can confirm that your website answers a request. A browser journey check can confirm that a particular customer task reaches its expected outcome. Synthetic user journey monitoring repeats those scripted tasks over time. The useful distinction is what each check observes: a successful response and a completed task answer different questions.
A small SaaS can benefit from both. Keep an availability signal, then add browser checks for the transitions customers depend on. A green homepage response does not establish that its search button works, its navigation opens the right page, or a form accepts valid input.
What HTTP 200 actually establishes
For a GET request, HTTP 200 indicates a successful request with a representation of the resource in the response body. It does not certify every interaction that might happen after the browser receives that resource. See the MDN explanation of HTTP 200.
For example, your server can deliver an HTML document successfully while the page's search handler does nothing. A check that only expects status 200 will pass. So might a response-body check for the word “Search,” because the button is still present in the HTML.
This is a limitation of those particular assertions, not a claim that every uptime service checks only status codes. Compare the configured checks, including content assertions and browser steps, before comparing product labels.
Also distinguish the public page from an API health endpoint. A successful response from /health reports whatever that endpoint implements. If it only returns a fixed response, it provides no direct evidence about search, database queries, or the public page's JavaScript.
Reproduce a broken task that still returns 200
Here is a deliberately small local example, not a production incident or a performance benchmark. It represents a documentation search. The required behavior is: entering invoices and selecting Search displays Found: Invoice export guide.
Save this as server.mjs and start it with node server.mjs. It requires Node.js and no extra server packages.
import { createServer } from 'node:http';
const html = `<!doctype html>
<meta charset="utf-8">
<label>Search documentation <input id="query"></label>
<button id="search">Search</button>
<p role="status" id="result"></p>
<script>
document.querySelector('#search').addEventListener('click', () => {
if (new URLSearchParams(location.search).has('broken')) return;
document.querySelector('#result').textContent =
document.querySelector('#query').value.trim() === 'invoices'
? 'Found: Invoice export guide'
: 'No results';
});
</script>`;
createServer((_request, response) => {
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(html);
}).listen(4173, '127.0.0.1');
The query parameter deliberately disables the interaction without changing the response status. Request both variants:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4173/
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:4173/?broken'
Each returns 200. Now, in a project with Playwright Test and its Chromium browser installed, save this as search.spec.js:
import { test, expect } from '@playwright/test';
for (const suffix of ['/', '/?broken']) {
test(`search produces a useful result: ${suffix}`, async ({ page }) => {
await page.goto(`http://127.0.0.1:4173${suffix}`);
await page.getByRole('textbox', {
name: 'Search documentation',
}).fill('invoices');
await page.getByRole('button', {
name: 'Search', exact: true,
}).click();
await expect(page.getByRole('status')).toHaveText(
'Found: Invoice export guide',
);
});
}
Run npx playwright test search.spec.js while the server is running. The broken variant is intentionally a failing test. The text assertion waits for the expected outcome instead of sleeping for an arbitrary duration; Playwright documents this behavior in its retrying assertions guide.
We exercised this fixture in a local Chromium browser. These were the observations:
| Variant | HTTP status | Browser outcome |
|---|---|---|
| Normal search | 200 | Expected result appears; assertion passes |
Search disabled with ?broken | 200 | Result remains empty; assertion fails |
Both observations are accurate. The server delivered the page in both cases. Only one variant completed the specified task. Clicking the button alone would not distinguish them: the useful check is the result after the click.
Compare coverage by the question being asked
Use this table to describe your checks rather than assuming that a provider's category determines its coverage.
| Configured check | Question it answers | Important limit |
|---|---|---|
| HTTP status check | Does this request receive the expected status? | Does not execute the page's controls |
| HTTP response-body assertion | Does the returned body contain the expected content? | Server HTML can contain controls that do nothing |
| API check with a result assertion | Does this API return the expected data for this input? | Does not exercise the browser integration |
| Browser page assertion | Does the expected page content become visible? | Does not establish later tasks work |
| Browser journey with outcome assertions | Can this selected sequence reach its expected result? | Covers the selected path, account, browser, and data |
A browser check can encounter a dependency failure when its path uses that dependency. That does not automatically identify which service caused the failure. Likewise, a passing API check and a failing browser check narrow the investigation, but do not by themselves prove the frontend is responsible: the requests or credentials may differ.
Choose a few customer outcomes to add
For a fictional invoicing SaaS, a useful first set might include public availability, navigation from the landing page to pricing, and documentation search. Each browser check should name its starting state and expected result. “Open pricing” is incomplete if any page counts as success; assert that the intended pricing heading and relevant plan information appear.
Authenticated invoice creation needs a separate decision about access and data. Use a controlled test identity and environment for changes that create records or trigger messages. Do not let a convenient public-page pass stand in for untested account or payment behavior.
Record the owner, environment, and response to failure alongside each check. If the task belongs in release verification, the production smoke testing checklist provides a way to organize those decisions.
Keep the limits attached to the result
A single successful run is evidence about that run. Recurring monitoring adds observations at the configured times and locations; it still leaves gaps between executions and outside the selected paths. Browser checks also do not establish security, accessibility, or correct permissions across the whole application.
When a journey fails, preserve the starting URL, failed action, expected result, actual result, and a screenshot. Those details help distinguish “page unavailable” from “page loaded but the task failed.” Keep the availability signal alongside that evidence so the team can assess the scope of the problem.
ReplaceQA is in closed beta for discovering journeys from a public URL, generating browser tests, and reviewing validation evidence. If that initial public-journey workflow fits your needs, request closed-beta access.