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:

FlowWhat it provesCan fail because of
Search: open, type, see resultsIndex files load and parseCDN rules, index size, CSP
Navigation: open mobile menu, follow 3 linksScripts run, routes resolveThird-party errors, redirects
Newsletter form: submit test addressEdge function and provider workProvider API, secrets, rate limits
Contact form: submit test messageFunction, spam filter, emailEmail provider, Turnstile
Assets: CSS and fonts load with 200Asset hosting and headersDeleted hashed assets, CORS

Five flows, each checking what it proves with explicit assertions rather than screenshots.

Layers of a static site covered by uptime checks compared with Playwright flows Four layers. The HTML response, checked by uptime monitors and Playwright. Static assets such as CSS, fonts and search index files, checked only by Playwright. Client-side behaviour such as menus and search, checked only by Playwright. Edge functions and third-party APIs behind forms, checked only by Playwright. What each kind of check can see HTML response status 200 uptime + Playwright Assets and search index CSS, fonts, index files Playwright only Client-side behaviour menus, search UI Playwright only Functions and third parties forms, newsletter Playwright only
Uptime checks see the top layer; the incidents on this site happened in the other three.

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.

How a synthetic check failure becomes an alert Five steps. A scheduled run executes the flows. A failing test retries once within the run. If it still fails, the run uploads the trace and screenshot. The alert script posts to the team channel. If the previous run also failed, it pages the on-call person. From failure to alert Scheduled run every 15 min + after deploy Retry once within the run Upload trace screenshot + network Channel post first failure Page on-call second in a row
Most transient failures stop at the retry or the channel post; only repeated failures page someone.

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:

IncidentFound byTime to detectReader reports before fix
Search index 404 after a CDN cache rule changeSearch flow6 min0
Newsletter provider API key rotated, function failingNewsletter flow14 min0
Third-party consent script error blocked mobile menuNavigation flow11 min (post-deploy run)0
Contact form spam filter rejecting all submissionsContact flow15 min1
Regional CDN issue, search failing in APACSingapore search check5 min0

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.

Time per scheduled monitoring run by step Vertical bars. Checkout and npm ci with cache 18 seconds. Installing Chromium 22 seconds. Running the five flows 40 seconds. Reporting 3 seconds. Caching the Playwright browser cut the install step to 4 seconds. Seconds per scheduled run 18 s Checkout + npm ci 22 s Install Chromium 4 s Browser cached 40 s Five flows 3 s Report
Median of 100 scheduled runs on a GitHub-hosted runner; the browser cache was added in week three.

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

MeasureValueSource
Runs per month~3,100 (Actions) + 8,600 (VM)Logs
Actions minutes per month~2,400Billing
Median time to detect real incidents11 minIncident notes
Paging false alarms, months 3–61Alert 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.

Filed underHow-tosPlaywright