Synthetic Monitoring With Scheduled Playwright Checks
A static site's HTML rarely breaks in production — it was tested before deploy, and the CDN serves the same bytes to everyone. What breaks is everything around it: the search index fails to load because a CDN rule changed, the newsletter form's edge function starts returning 500s after a provider update, a third-party script throws and stops the navigation menu opening, a certificate renewal fails on an asset subdomain. Uptime checks against the home page see none of this, because the home page still returns 200.
Synthetic monitoring with Playwright runs the flows readers actually use — search, navigation, forms — against the live site on a schedule, and alerts when one fails. This guide sets it up for a documentation site on Cloudflare with Pagefind search, a newsletter form on a Worker and a contact form, using scheduled GitHub Actions and a small dedicated runner for the most important check. It belongs to Monitoring Static Sites in Production; simple uptime and certificate checks are covered in Uptime and Synthetic Checks for Static Sites.
Prerequisites
- A Playwright test suite, or willingness to write a few flows (each is 10–30 lines).
- A test mode or test address for forms, so monitoring submissions are recognisable and discarded.
- An alert destination: a chat channel, an incident tool, or email.
Step 1: Choose the Flows
The checks should cover the parts of the site that can fail independently of the HTML:
| Flow | What it proves | Can fail because of |
|---|---|---|
| Search: open, type, see results | Index files load and parse | CDN rules, index size, CSP |
| Navigation: open mobile menu, follow 3 links | Scripts run, routes resolve | Third-party errors, redirects |
| Newsletter form: submit test address | Edge function and provider work | Provider API, secrets, rate limits |
| Contact form: submit test message | Function, spam filter, email | Email provider, Turnstile |
| Assets: CSS and fonts load with 200 | Asset hosting and headers | Deleted hashed assets, CORS |
Five flows, each checking what it proves with explicit assertions rather than screenshots.
Step 2: Write Monitoring-Grade Tests
Monitoring tests differ from CI tests in two ways: they run against production, and every failure should mean something. They wait for specific outcomes and fail with a clear message:
// monitor/search.spec.ts
import { test, expect } from '@playwright/test';
test('search returns results for a known query', async ({ page }) => {
await page.goto('https://docs.example.com/', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'Search' }).click();
await page.getByRole('searchbox').fill('deploy');
await expect(page.getByRole('option').first(), 'search produced no results').toBeVisible({ timeout: 8000 });
});
test('newsletter signup works end to end', async ({ page }) => {
await page.goto('https://docs.example.com/newsletter/');
await page.getByLabel('Email').fill(`monitor+${Date.now()}@example.com`);
await page.getByRole('button', { name: 'Subscribe' }).click();
await expect(page.getByRole('status')).toHaveText(/check your inbox/i, { timeout: 10000 });
});
The newsletter Worker recognises monitor+ addresses, runs every step up to the provider call, calls the provider's validation endpoint instead of creating a subscriber, and returns the normal success response. The contact form does the same with a test flag that skips the final email send but exercises Turnstile verification with a test key.
Step 3: Schedule Them
name: synthetic
on:
schedule: [{ cron: '*/15 * * * *' }]
workflow_dispatch:
workflow_run: { workflows: [deploy], types: [completed] }
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci && npx playwright install --with-deps chromium
- run: npx playwright test -c monitor/playwright.config.ts
- if: failure()
uses: actions/upload-artifact@v4
with: { name: monitor-report, path: monitor/report, retention-days: 7 }
- if: failure()
run: node monitor/alert.mjs # posts to the incident channel with the report link
The workflow_run trigger runs the suite right after each deploy as a smoke test, in addition to every 15 minutes. The Playwright config sets retries: 1 and records a trace and screenshot on failure, so the alert links to exactly what the browser saw.
Step 4: Route Alerts by Confidence
A single failure posts to a chat channel; two consecutive failures of the same test page the on-call person. The alert script reads the previous run's result from the artifact store to decide. In the first two months that rule cut paging alerts from 9 to 2 without missing a real incident — the other seven were single transient failures that passed on the next run.
Step 5: Add a Second Location for the Critical Flow
GitHub-hosted runners run from a few US and EU regions. Search failures had once affected only Asia-Pacific readers (a regional CDN issue), which the Actions-based check could not see. The search check now also runs every 5 minutes from a small VM in Singapore with a cron job and the same test file, posting to the same alert script. That VM costs about $5 a month.
What It Caught
In six months the checks found five real incidents:
| Incident | Found by | Time to detect | Reader reports before fix |
|---|---|---|---|
| Search index 404 after a CDN cache rule change | Search flow | 6 min | 0 |
| Newsletter provider API key rotated, function failing | Newsletter flow | 14 min | 0 |
| Third-party consent script error blocked mobile menu | Navigation flow | 11 min (post-deploy run) | 0 |
| Contact form spam filter rejecting all submissions | Contact flow | 15 min | 1 |
| Regional CDN issue, search failing in APAC | Singapore search check | 5 min | 0 |
None of the five changed the HTML or failed the uptime check.
Cost and Run Time
The five flows run in about 40 seconds in a warm job, but each scheduled run also installs dependencies and a browser, which dominated cost.
Caching ~/.cache/ms-playwright keyed on the Playwright version brought the browser install down to a 4-second restore, and runs to about a minute — roughly 2,400 Actions minutes a month at a 15-minute interval plus post-deploy runs. Running the same suite on the always-on Singapore VM costs nothing per run, which is why the more frequent 5-minute search check lives there.
Measured Impact
| Measure | Value | Source |
|---|---|---|
| Runs per month | ~3,100 (Actions) + 8,600 (VM) | Logs |
| Actions minutes per month | ~2,400 | Billing |
| Median time to detect real incidents | 11 min | Incident notes |
| Paging false alarms, months 3–6 | 1 | Alert log |
Pitfalls & Rollback
- Forms that send real emails or create real subscribers. Add a recognisable test mode.
- Fixed waits. Wait for elements and responses; retry once.
- Paging on every failure. Require two consecutive failures.
- One location. Regional problems need a second vantage point for critical flows.
- Rollback: the workflow can be disabled from the Actions tab in seconds; the tests themselves are harmless to production.
Conclusion
The parts of a static site that fail in production are the ones uptime checks cannot see: search index files, client-side scripts, edge functions and third-party APIs. Five Playwright flows run every 15 minutes and after every deploy, with a retry, a second location for search and paging only on repeated failures, found five incidents in six months before readers reported them — none of which changed a byte of HTML.
FAQ
Why use Playwright for monitoring when uptime checks exist?
Uptime checks confirm a URL returns 200. They do not notice that search returns no results, that a form submits but the function behind it fails, that the CSS file is missing, or that a third-party script broke navigation. Playwright drives a real browser through the same flows readers use, so it catches failures in the parts of a static site that are not static.
How often should synthetic checks run?
Often enough that a failure is found before many readers hit it, and not so often that cost and noise grow. Every 10 to 15 minutes for the main flows suits most content sites. Running more checks after a deploy, as a smoke test, catches deploy-related failures within minutes.
Can GitHub Actions run scheduled Playwright checks?
Yes, with a scheduled workflow, and it is free within the plan's minutes for public repositories and modest for private ones. Scheduled workflows can be delayed by several minutes and only run from GitHub's regions, so for tight intervals or multiple locations a dedicated runner or a monitoring service that runs Playwright scripts is better.
How do I avoid false alarms from synthetic checks?
Wait for specific elements rather than fixed times, retry a failed check once before alerting, require two consecutive failures for paging alerts, and run from more than one location when possible. Most false alarms come from timing assumptions and single transient network errors.
Related
- Parent: Monitoring Static Sites in Production — watching a live site.
- Uptime and Synthetic Checks for Static Sites — simpler checks to run alongside.
- Testing Search and Navigation Flows With Playwright — the same flows tested before deploy.
- Newsletter Signup with an Edge Function — the function this monitors.
- Running Smoke Tests Against a Preview URL — checks before the deploy goes live.
Filed underHow-tosPlaywright