Playwright MCP token cost vs reading pages as markdown
We drove the real Playwright MCP server and FastAF markdown over 11 public pages and counted tokens. Median page: 9,874 vs 5,083, with exceptions both ways.

A Playwright MCP page read costs a median of 9,874 tokens in our test, against 5,083 for the same pages as markdown from FastAF. That is about twice the tokens, not a hundred times. The spread is wide, though: from 0.4x on one page to 4x on another. We measured 11 public pages on 19 October 2026 with @playwright/mcp 0.0.83 and counted tokens with tiktoken o200k_base.
We could not find a per-page measurement of Playwright MCP that we could reproduce, so we ran one. The method and script are below, along with the cases where the browser is the right tool.
How many tokens does Playwright MCP use per page?
We started the real server (@playwright/mcp 0.0.83, the latest on npm as of this date, running Playwright 1.64 alpha) headless over stdio, with the official MCP Python SDK as the client. For each page the client called browser_navigate and then browser_snapshot, and we counted every character the model would have received. In 0.0.83 browser_navigate returns only a link to a snapshot file (78 to 168 tokens), so reading the page takes the second call. The other arm was FastAF /v1/scrape with formats=markdown, plain first and dynamic=true only when the plain call failed or the check phrases were missing.
“Worked” means two phrases fixed in advance were present in what the model would see. Tokens are for run 1 of 3 and cover navigate plus snapshot.
| Page | Raw HTML | Playwright snapshot | Playwright --mobile |
FastAF markdown |
|---|---|---|---|---|
| MDN, Fetch API (docs) | 41,439 | 7,481 | 6,367 | 4,110 |
| Python docs, asyncio tasks (long docs) | 50,276 | 34,405 | 33,354 | 15,236 |
| Guardian article (news) | 101,462 | 6,343 | 6,129 | 3,585 |
| IKEA BILLY bookcase (product) | 336,714 | 10,017 | 7,812 | 5,092 |
| Wikipedia, Model Context Protocol | 79,923 | 24,931 | 3,864 (folded) | 21,784 |
| GitHub, python-sdk (repo) | 135,152 | 9,874 | 6,807 | 5,083 |
| crates.io, serde (JS-rendered app) | n/a (404) | 4,693 | 4,767 | 1,408 (5 credits) |
| Hacker News front page (listing) | 11,694 | 13,649 | 13,649 | 3,365 |
| Anthropic engineering blog post | 51,697 | 11,689 | 11,447 | 6,115 |
| PyPI, requests (package) | 73,730 | 7,931 | 4,840 (thin) | 20,165 |
| GOV.UK VAT rates | 19,415 | 3,378 | 3,337 | 1,392 |
| Median, 11 pages | 51,697 | 9,874 | 6,367 | 5,083 |
| Total, 11 pages | 901,502 | 134,391 | 102,373 | 87,335 |
“Raw HTML” is a plain GET, for scale. crates.io returned a 404 shell to it. Timing was close: a warm Playwright page took a median of 0.5 s (the first call after launch added 0.4 to 0.9 s), and FastAF a median of 0.5 s over three runs, with dynamic rendering at 6 to 7 s. Playwright token counts moved by under 2% between runs apart from the Guardian (5,671 to 6,343) and the GitHub page (9,376 to 9,874). FastAF’s Wikipedia output was 21,784 tokens in two calls and 9,513 in two others, so treat that row as a range.
Why is a snapshot bigger than markdown?
A snapshot is a YAML accessibility tree. Every node carries a role, a nesting level and a [ref=...] id so the agent can click it, and every link spends a line on its - /url: target. We counted the URL lines alone at 18% of snapshot tokens across the pages (9% on the Python docs, 25% on Hacker News). Hacker News is the clearest case: its table layout becomes table > rowgroup > row > cell several levels deep before the first story, and the snapshot is 4x the markdown.
Markdown has no refs and almost no structure, so prose-heavy pages come out near 2x smaller. Two pages did not follow that pattern. PyPI’s markdown (20,165) kept the whole page including the file list and classifiers, while the snapshot (7,931) did not. Wikipedia’s markdown was within 13% of the snapshot. The honest summary is that the snapshot is a bit bigger on most pages and the structure is what you pay for, but a page with a lot of text can cost more as markdown.
What does that cost in dollars?
Anthropic’s pricing page lists Claude Sonnet 5.5 at $2 per million input tokens and Claude Fable 5.1 at $10 (both as of 19 October 2026, pricing page). Our counts use an OpenAI tokenizer, so Claude’s own counts will differ somewhat; the ratios are what to trust. At the median page:
| Per 1,000 page reads | Tokens | Sonnet 5.5 input | Fable 5.1 input |
|---|---|---|---|
| Raw HTML | 51.7 M | $103.39 | $516.97 |
| Playwright MCP snapshot | 9.9 M | $19.75 | $98.74 |
Playwright MCP, --mobile |
6.4 M | $12.73 | $63.67 |
| FastAF markdown | 5.1 M | $10.17 plus 1,000 credits | $50.83 plus 1,000 credits |
On the Starter pack ($10 for 20,000 credits) 1,000 plain scrapes are $0.50, so FastAF lands near $10.70 on Sonnet against $19.75 for the snapshot. That is about half, and it is input only. A page read also stays in the context for every later turn unless you cache or drop it, so a long run pays for it more than once. Tool definitions are a fixed cost per session: Playwright MCP lists 25 tools (5,250 tokens), the FastAF MCP server 15 (3,831).
Can you make Playwright MCP cheaper?
Yes, and the package README says so itself: it recommends Playwright CLI with skills for coding agents because it keeps accessibility trees out of the model’s context. Within MCP, the --mobile flag (documented as saving tokens) cut the median from 9,874 to 6,367 here. It has a catch. On Wikipedia it dropped 24,931 tokens to 3,864 because the mobile layout folds sections: 1 paragraph in the snapshot instead of 22. On PyPI it lost the license field and failed our check. Do not read a smaller snapshot as the same content.
Other levers exist in the tool list and we did not benchmark them: browser_find returns matching nodes with context instead of the whole tree, browser_snapshot takes a depth and a filename, and browser_navigate already writes to a file rather than into the chat. If you know which part of the page you need, those will beat any whole-page read.
When is the browser the right tool?
When the task is interaction: clicking through a UI, filling a form, a flow behind a login that you own, a page where the next step depends on what the last click did. The refs in the snapshot exist for that, and markdown has nothing to click.
We also tried a Stack Overflow question and left it out of the table: it answered with a bot-check page (HTTP 403), and FastAF returned 400 and 422 target_unavailable with no charge. A site’s bot checks are the owner’s decision; FastAF reads public pages and does not try to get past them, and we did not benchmark tools against them.
JavaScript apps are the middle case. On crates.io the first Playwright snapshot was 35 tokens because the app had not loaded; a second one after a wait gave 4,693. FastAF’s plain call returned 422, and the render at dynamic=true cost 5 credits and 1,408 tokens in 6.3 s. See why a fetch comes back empty and JavaScript rendering. One correction: our first check phrase for crates.io did not match how the page splits its text, so we fixed it and re-ran both tools on that page.
When the job is to read a public page and answer from it, reading is enough, and the web scraping API or the scrape_page tool does it in one call. For the wiring in Claude Code, Cursor and Codex, see web search over MCP.
How do I reproduce this?
import asyncio, os, sys, requests, tiktoken
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
enc = tiktoken.get_encoding("o200k_base")
tokens = lambda s: len(enc.encode(s, disallowed_special=()))
def fastaf_markdown(url, dynamic=False):
params = {"url": url, "formats": "markdown"}
if dynamic:
params.update(dynamic="true", wait=3000)
r = requests.get("https://api.fastaf.co/v1/scrape", params=params, timeout=120,
headers={"Authorization": f"Bearer {os.environ['FASTAF_API_KEY']}"})
return r.text if r.status_code == 200 else "", r.headers.get("X-FastAF-Credits-Used")
async def playwright_snapshot(urls):
# npm i @playwright/mcp@0.0.83 (the real server, over stdio, headless)
server = StdioServerParameters(command="node", cwd=os.getcwd(),
args=["node_modules/@playwright/mcp/cli.js", "--headless", "--isolated"])
out = {}
async with stdio_client(server) as (r, w), ClientSession(r, w) as s:
await s.initialize()
for url in urls:
nav = await s.call_tool("browser_navigate", {"url": url})
snap = await s.call_tool("browser_snapshot", {})
text = lambda res: "\n".join(c.text for c in res.content if c.type == "text")
out[url] = tokens(text(nav)) + tokens(text(snap))
return out
urls = sys.argv[1:]
pw = asyncio.run(playwright_snapshot(urls))
for url in urls:
md, credits = fastaf_markdown(url)
print(f"{url}\n playwright {pw[url]:>7,} tokens fastaf {tokens(md):>7,} tokens ({credits} credit)")
Run on 19 October 2026 against two of the pages above, it printed:
https://www.gov.uk/vat-rates
playwright 3,038 tokens fastaf 1,392 tokens (1 credit)
https://news.ycombinator.com/
playwright 13,647 tokens fastaf 3,323 tokens (1 credit)
GOV.UK’s snapshot came out at 3,038 here against 3,378 in the table because pages change between runs. Our full run added the needle checks, one retry after a 3 s wait for thin snapshots, three repetitions and the --mobile arm. Throughout, we ran Chromium on a laptop in Italy, and the same URLs are the ones in the table, so you can rerun them.
The short version
On 11 public pages a Playwright MCP read took a median of 9,874 tokens and the markdown 5,083: roughly half, not a hundredth. The snapshot spends tokens on refs, roles and URLs; --mobile trims it but can drop content; and the snapshot can still win on pages that are mostly structure. Use the browser when you need to click, type or sign in to something of your own, and read the page as markdown when you only need what it says. Get 500 free credits and run the script on the pages your agent reads most. Prices and limits are on the pricing page and in the docs.