Use Browser Run to capture a screenshot after a selected request. Save the image in R2 while returning the original response to the user.
The browser makes an independent visit to a fixed public URL. It does not inherit the user's cookies, authorization, Gateway session, or egress configuration. The result is a screenshot of the public page, rather than an exact capture of what the user saw. Use this template for a public page you control.
In your Worker project, install @cloudflare/zt-types ↗︎ version 2026.10.1 or later as a development dependency:
npm i -D @cloudflare/zt-typesyarn add -D @cloudflare/zt-typespnpm add -D @cloudflare/zt-typesbun add -d @cloudflare/zt-typesKeep the import type declarations for GatewayMiddlewareContext and GatewayMiddlewareNext in src/index.ts. They describe the Gateway context and continuation and are removed when TypeScript compiles. Import the runtime WorkerEntrypoint class from cloudflare:workers.
Create a private R2 bucket named gateway-screenshots, then add browser and R2 bindings to the Worker configuration:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"browser": {
"binding": "BROWSER"
},
"r2_buckets": [
{
"binding": "CAPTURES",
"bucket_name": "gateway-screenshots"
}
]
}[browser]
binding = "BROWSER"
[[r2_buckets]]
binding = "CAPTURES"
bucket_name = "gateway-screenshots"The browser binding's quickAction() method requires a compatibility date of 2026-03-24 or later. Refer to Browser Run configuration.
Replace https://example.com/ with a public test page you control. The local browser interface includes only the Quick Action used here.
import { WorkerEntrypoint } from "cloudflare:workers";
const PAGE = "https://example.com/";
export class GatewayMiddleware extends WorkerEntrypoint {
async handle(request, context, next) {
const response = await next(request);
const url = new URL(request.url);
if (
request.method === "GET" &&
url.origin + url.pathname === PAGE &&
response.ok
) {
this.ctx.waitUntil(
this.capture().catch(() => console.error("Screenshot capture failed")),
);
}
return response;
}
async capture() {
const image = await this.env.BROWSER.quickAction("screenshot", {
url: PAGE,
viewport: { width: 1280, height: 720 },
gotoOptions: { timeout: 10_000 },
});
if (!image.ok) {
await image.body?.cancel();
throw new Error("Screenshot request failed");
}
await this.env.CAPTURES.put(
`${crypto.randomUUID()}.png`,
await image.arrayBuffer(),
{
httpMetadata: { contentType: "image/png" },
},
);
}
}import { WorkerEntrypoint } from "cloudflare:workers";
import type {
GatewayMiddlewareContext,
GatewayMiddlewareNext,
} from "@cloudflare/zt-types";
const PAGE = "https://example.com/";
interface Env {
CAPTURES: R2Bucket;
BROWSER: {
quickAction(
action: "screenshot",
options: {
url: string;
viewport: { width: number; height: number };
gotoOptions: { timeout: number };
},
): Promise<Response>;
};
}
export class GatewayMiddleware extends WorkerEntrypoint<Env> {
async handle(
request: Request,
context: GatewayMiddlewareContext,
next: GatewayMiddlewareNext,
): Promise<Response> {
const response = await next(request);
const url = new URL(request.url);
if (
request.method === "GET" &&
url.origin + url.pathname === PAGE &&
response.ok
) {
this.ctx.waitUntil(
this.capture().catch(() => console.error("Screenshot capture failed")),
);
}
return response;
}
async capture(): Promise<void> {
const image = await this.env.BROWSER.quickAction("screenshot", {
url: PAGE,
viewport: { width: 1280, height: 720 },
gotoOptions: { timeout: 10_000 },
});
if (!image.ok) {
await image.body?.cancel();
throw new Error("Screenshot request failed");
}
await this.env.CAPTURES.put(
`${crypto.randomUUID()}.png`,
await image.arrayBuffer(),
{
httpMetadata: { contentType: "image/png" },
},
);
}
}The script uses a fixed browser destination and does not copy query parameters or session credentials from the user's request. The viewport limits the image to a regular viewport screenshot.
Deploy the Worker and create a Forward policy for the test page's Host, URL Path, and HTTP Method GET, scoped to your test user group.
Visit the page through Gateway. Confirm that the page loads normally and a PNG object appears in R2. Open the object to check the screenshot. Browser Run Quick Actions require a remote browser binding. They cannot be validated entirely with local emulation.
Screenshots consume Browser Run usage. Scope or sample captures to control volume. Browser queueing, navigation, and the R2 write must fit within background execution limits. Use Queues or Workflows for reliable captures of slow pages.