The agent found the bug on tool call 6. It said so, out loud, in its reasoning: the signup form submits twice if you hit Enter before the debounce clears. Then it kept clicking around. Three tool calls later the context compacted, and when I asked for the report it told me the signup flow looked fine.
It had not lied. It genuinely did not have the memory anymore. Playwright MCP token usage had quietly evicted the one useful thing the run produced.
I spent an afternoon measuring where all of it went. Nine tool calls, 124,300 tokens, 62% of a 200,000-token window, and not one of those tokens was a screenshot.
TL;DR
-
Playwright MCP token usage is dominated by
browser_snapshot, not screenshots. Every navigate/click/type returns a fresh accessibility tree of the whole page. - On my app, one snapshot averaged 13,800 tokens. A 1280x800 screenshot of the same page costs about 1,400. The "expensive" image is roughly 10x cheaper than the text.
- Nine tool calls in one signup flow returned 124,300 tokens of near-duplicate page state. Baseline: 9 of 12 runs finished without compacting.
- Naively filtering the snapshot made it worse (7 of 12), because clicks reference
ref=handles that live inside the part I was throwing away. - Preserving refs, dropping everything else, and running one page per subagent: 11 of 12 runs, peak context 138k → 44k, median run 4m12s → 2m38s, my logged spend about $0.38 → $0.12 per run.
Why does Playwright MCP use so much context?
Because the default way the agent sees a page is text, and that text is the entire accessibility tree.
browser_snapshot returns something like a YAML dump of every ARIA node on the page: role, name, and a ref=e47 handle the agent needs in order to click anything. On a marketing page that is small. On an app screen with a data table, a sidebar, a modal root, and three icon-button toolbars, it is enormous.
The part that actually hurts is that most action tools return a snapshot too. Click a button, get a snapshot. Type into a field, get a snapshot. So a flow that is conceptually "go here, fill this, submit" is really nine full-page serializations, eight of which differ from the previous one by about four lines.
You are not paying for observation. You are paying for the same observation nine times.
How do you measure Playwright MCP token usage?
Put a proxy between the client and the MCP server and log the size of every result. MCP over stdio is newline-delimited JSON-RPC, so a passthrough wrapper is about fifteen lines.
// mcp-tap.js — spawn the real server, log every result size
import { spawn } from "node:child_process";
import readline from "node:readline";
const child = spawn("npx", ["@playwright/mcp@latest", ...process.argv.slice(2)],
{ stdio: ["pipe", "pipe", "inherit"] });
process.stdin.pipe(child.stdin);
readline.createInterface({ input: child.stdout }).on("line", (line) => {
try {
const msg = JSON.parse(line);
const text = JSON.stringify(msg.result?.content ?? "");
if (msg.result) console.error(`[tap] id=${msg.id} chars=${text.length}`);
} catch {}
process.stdout.write(line + "\n");
});
Point your MCP config at node mcp-tap.js instead of the server, and stderr gives you a per-call size log that does not interfere with the protocol. (Write to stderr, never stdout. One stray console.log on stdout and the connection dies.)
Characters are not tokens, so I ran the captured payloads through a token count afterward. The snapshots landed around 3.4 characters per token, which is what you would expect from structured text with a lot of short repeated keys.
Here is the same signup flow, measured:
| Tool call | What it did | Result tokens |
|---|---|---|
| 1 |
browser_navigate to /
|
8,100 |
| 2 |
browser_click "Sign up" |
2,900 |
| 3 |
browser_type email |
3,050 |
| 4 |
browser_type password |
3,100 |
| 5 |
browser_click submit |
11,600 |
| 6 |
browser_navigate /onboarding
|
16,900 |
| 7 |
browser_click "Skip for now" |
21,400 |
| 8 |
browser_click "Add project" |
22,800 |
| 9 |
browser_snapshot (agent double-checking) |
22,800 |
124,650 tokens. Calls 8 and 9 are byte-for-byte identical because nothing on the page changed. The agent asked for a snapshot it already had, and paid full price for the reprint.
Is browser_take_screenshot cheaper than browser_snapshot?
Yes, by roughly an order of magnitude, and that surprised me enough that I re-measured it twice.
Image cost scales with pixels. A 1280x800 viewport screenshot works out to a bit under 1,400 tokens. My average page snapshot was 13,800. Swapping every observation in that flow for an image would have been about 12,300 tokens instead of 124,650.
The catch is real, though: the agent cannot click a pixel. It needs the ref= handles from the snapshot to drive the page. So "just use screenshots" is not a fix, it is a trade. Screenshots answer is this broken visually. Snapshots answer what can I interact with. Most agent runs ask the second question nine times when they only needed it twice.
What actually fixed it, and what it broke
Three changes, in the order I tried them.
1. A filtering proxy — this made things worse. My first version truncated any snapshot over 4,000 tokens. Result: 7 of 12 runs completed, down from a 9 of 12 baseline. The agent would read a trimmed tree, pick the closest matching element, and click a ref that had been cut. The server returns a ref-not-found error, the agent retries with a slightly different guess, and four calls later it gives up. Truncation does not degrade gracefully here. It corrupts the one thing the snapshot exists to provide.
2. A filter that preserves refs. Second version keeps every node that has a ref=, plus headings and any node whose name matched the task text, and drops the rest (decorative containers, empty generics, repeated table rows past the first three). Same nine calls: 18,600 tokens instead of 124,650. The agent could still click everything it could see, because everything clickable survived.
3. One page per subagent. Even filtered, state accumulates. I moved each page of the flow into its own subagent with a one-paragraph brief and had it return a short finding instead of a tree. The parent context now holds nine sentences, not nine page dumps.
Final config, 12 runs: 11 completed. The one failure was a subagent that reported "form submitted successfully" for a page that had a validation error below the fold, because I had given it a screenshot budget of zero. My fault, not the tool's.
Totals: 36 runs across three configurations, about three hours including the measuring. Peak context per full run went 138k → 44k. Median wall clock 4m12s → 2m38s, mostly because fewer tokens means faster time to first token on every turn. Logged spend per run, from my own usage data, went from roughly $0.38 to roughly $0.12.
Should you use Playwright MCP at all?
Yes. It is the fastest way I have found to let an agent actually drive a browser, and nothing here is a bug in it. Serializing the whole accessibility tree is the correct default when the server has no idea what your task is. The mismatch is that you do know, and the default cannot use that.
Two things I would do on day one now. Check your version's flags before writing any proxy, since capability and vision options move around between releases and a supported knob beats my duct tape. And put a size log on the connection immediately, because "my agent ran out of context" is unactionable while "call 7 returned 21,400 tokens of sidebar" tells you exactly what to cut.
The thing I keep thinking about is that first run. The agent did the job. It found a genuine double-submit bug on call 6 and then spent the rest of its context overwriting the memory of having found it. That failure does not look like a crash. It looks like a clean report that says everything is fine.
So why does Playwright MCP eat your context window? Because every navigate, click, and type returns a full accessibility-tree snapshot of the page, and on a real app that snapshot runs 13,800 tokens against roughly 1,400 for a screenshot of the same screen. Nine ordinary tool calls in my signup flow returned 124,650 tokens, 62% of a 200k window, with two of the nine byte-identical. Filtering the snapshot down to ref-carrying nodes and running one page per subagent cut the same flow to 18,600 tokens and took completion from 9 of 12 runs to 11 of 12. Do not truncate blindly: clicks depend on ref= handles, and cutting them turns a large context problem into a broken agent.
Written by the developer behind Preterview, an interview prep platform.
Top comments (0)