---
title: 'Every browser check gets its own machine'
description: 'How Yorker keeps checks isolated from each other and from our own infrastructure: a single-use microVM for every browser run, per-customer containers for HTTP and MCP checks, and outbound-only agents for private locations.'
date: '2026-09-28'
author: 'Drew Post'
tags: ['security', 'architecture', 'isolation', 'synthetic-monitoring', 'infrastructure']
canonical_url: 'https://yorkermonitoring.com/blog/every-browser-check-gets-its-own-machine'
---

![Stacked, standardized shipping containers in blue, red and orange, each sealed and numbered separately.](/blog/every-browser-check-gets-its-own-machine/00-hero.jpg)

A synthetic monitoring platform runs other people's code. A browser check in Yorker can be a full Playwright script: it navigates, clicks, calls `fetch`, and does whatever its author wrote. That script runs on shared infrastructure, next to checks from customers who have never agreed to trust each other, and it has to finish without being able to see anything that belongs to anyone else.

The rule we designed around is simple. How strongly a check is isolated depends on what it executes. A check that runs customer code gets a machine of its own for exactly one run. A check that only sends requests we build from its configuration runs in a long-lived container. A check on a private location runs on the customer's own hardware and only ever calls out.

![Flow chart of where each check type runs. The Yorker control plane dispatches browser checks to a single-use microVM running Chromium and the check script, which sends a signed result back. HTTP and MCP checks run in a long-lived container that polls the control plane outbound. Private location agents run inside the customer's network, poll the control plane outbound, and send raw OTLP to the customer's own backend. All three reach the check targets.](/blog/every-browser-check-gets-its-own-machine/01-where-checks-run.svg)

## Browser checks: one machine, one run

Every browser check run gets a new hardware-virtualized microVM, created for that run and destroyed when it ends, whether the check passed, failed or timed out. It has its own kernel, filesystem and network stack. Nothing carries over from a previous run because there is no previous run on that machine.

We could have kept a pool of warm machines and reused them, and checks would start faster. The problem is that reuse makes isolation depend on cleanup: every cookie, cache entry, temp file and process left by the last customer's script has to be found and removed, every time, without a miss. A machine that has only ever run one check doesn't need cleaning. We pay a cold start on every browser run to get that property, and for code we didn't write it's the right trade.

Inside the machine we treat the script as hostile too. The runner restricts what a script can do to the Node.js process around it before any customer code loads, and the check's deadline is enforced from outside the script, so a script that hangs is stopped rather than trusted to exit.

## Credentials that expire with the run

A machine that runs untrusted code has to be designed as if it will be compromised, so the useful question is what an attacker gets if they own one.

Each run is dispatched with credentials minted for that run alone. They identify the run, the check and the team, they're signed with a key the machine never holds, and they expire within minutes. The control plane verifies the signature and every bound identifier before it accepts a result, so a result can't be submitted for another run or another customer. Screenshots go to object storage with a credential that only reaches that run's own path.

If a result never arrives, the orchestrator doesn't wait on the machine. It tracks every dispatched run against a deadline and destroys the machine itself when the deadline passes.

![Sequence diagram of one browser check run across the orchestrator, the compute API, the single-use microVM, object storage and the control plane. The orchestrator mints run-scoped credentials and asks the compute API for a new machine. The machine boots, runs the check against the target, uploads screenshots under the run's own path and posts a signed result. The control plane verifies the signature and run binding before storing the result, and the orchestrator destroys the machine. If no result arrives by the deadline, the orchestrator destroys the machine anyway.](/blog/every-browser-check-gets-its-own-machine/02-one-run-sequence.svg)

## HTTP and MCP checks: long-lived containers

HTTP and MCP checks don't run customer code. Yorker builds each request from the check's configuration: a URL, headers, a body and assertions, or for MCP, a JSON-RPC session and the tools to call. That's a much smaller attack surface, so these checks run in long-lived containers rather than a new machine per run. On paid plans, each team gets its own container in each region.

Every outbound request from these checks is validated before it's sent. Private address ranges, loopback and cloud metadata endpoints are refused, and the validation runs again on every redirect, so a hostname that looks public when a check is saved can't be pointed somewhere internal later.

## Private locations: outbound only

A private location is the same runner, deployed on infrastructure the customer runs. It has one connection to Yorker, and the agent opens it: it polls over HTTPS for its checks and posts results back. There's no inbound path from us, so there's nothing to open in the customer's firewall.

Raw OTLP from a private location goes straight from the agent to the customer's own endpoint. Check results still come back to Yorker, because that's where scoring, SLOs and alerting run before the enriched telemetry goes on to the customer's backend. Screenshots go to the customer's own storage bucket.

## What it costs

Isolation like this isn't free, so we chose where to spend it.

- Every browser run starts a cold machine, which adds start-up time to each check.
- Compute cost for browser checks grows with the number of runs, because nothing is shared between them.
- HTTP and MCP checks get the cheaper long-lived model because they don't execute customer code. That's the line we use to decide which model a check type gets.

[More on how browser checks work](/features/browser-monitoring)
