← all articles

What silently doubled a browser agent's token bill after an upgrade

You bump a dependency from one minor version to the next. The tests pass. The task success rate looks the same. A week later the invoice from your model provider is roughly twice what it was, and nothing in your own code changed.

This is the failure I want to pull apart, because it keeps happening to people running browser agents. The reader question that prompted this piece is browser-use issue 3768, which asks, in essence, why spend jumped after an upgrade. I’m going to answer it directly, but with a caveat up front: I am not going to claim a root cause for that specific thread that I haven’t reproduced myself. What I can give you is the mechanism that produces this class of regression, the places in a browser agent’s context construction where a version change can double input tokens without any error, and an audit method you can run against any two versions in an afternoon.

The stakes are real because browser agents are input-heavy. Every step re-sends the task, the history, the tool schema, a serialized view of the page, and often a screenshot. Output tokens are a rounding error next to that. So a change that adds a few thousand tokens to every step, or one that quietly breaks prompt caching, shows up as a multiplier on your whole bill. And since the agent still works, nothing alerts you.

background and prior art

A browser agent loop is simple to describe. Observe the page, build a prompt, call the model, parse an action, execute it, repeat. Libraries like browser-use wrap this loop and own the “build a prompt” step for you. That is the convenience, and it is also the risk: the prompt is assembled inside the library, so an upgrade can change it without touching your code.

The prompt for a given step is usually made of these parts:

  • system prompt: static instructions about how to act, often a few thousand tokens
  • tool or action schema: the list of actions the model can call, serialized as JSON schema
  • task and memory: the user’s goal plus whatever the agent has chosen to retain
  • step history: previous actions and their results, which grows with each step
  • page state: a text serialization of the DOM, usually a list of interactive elements with indexes
  • screenshot: an image, optionally annotated with element boxes

Each of these has a way to grow silently. I covered the cost side of this in cost engineering for computer use agents, and the short version is that the per-step prompt is the unit you have to measure, not the per-task total. A per-task total hides which component moved.

The other piece of background is prompt caching. Anthropic’s prompt caching documentation describes how a stable prefix of your prompt can be cached and re-read at a lower rate than fresh input. The key property is that the cache matches on an exact prefix. If anything early in the prompt changes between steps, everything after it is a cache miss and gets billed as fresh input. Check the provider’s current pricing page for the exact cache read discount, since those numbers change (as of October 2026 I’d treat any figure from memory as stale). What matters for this article is the structure: a stable prefix is cheap, an unstable prefix is not, and a library upgrade can move content from one category to the other.

There is a wider pattern here too. I wrote about a similar silent-change problem in why upgrading Playwright can silently break CORS-dependent requests your agent relies on. Different symptom, same root issue. A dependency whose job is to hide a detail from you will occasionally change that detail, and your integration tests were never checking it.

the core mechanism

When people say “the token bill doubled”, there are really only four ways to get there. Input tokens per step go up, the number of steps goes up, caching stops working, or the number of model calls per step goes up. Every real incident I’ve seen is one of these or a combination. Let me go through each as it applies to a context-construction change.

per-step prompt growth

This is the most direct. A new version might change how the page is serialized. For example, it might include more attributes per element, include elements that were previously filtered as non-interactive, include hidden or off-screen nodes, or stop truncating long text. On a content-heavy page, a serializer change of that kind can multiply the page-state portion several times over.

It can also change history handling. If the older version summarized or dropped old step results and the newer version keeps them verbatim, the prompt grows linearly with step count instead of staying roughly flat. That one is nasty because short test tasks (five steps) look fine, while real tasks (thirty steps) blow up.

screenshot handling

Images are billed as tokens. Anthropic’s vision documentation gives an approximation of image tokens as width times height divided by 750, with images above a size limit being resized first. So a 1280 by 800 screenshot costs around 1,365 tokens, and a 1920 by 1080 one costs around 2,765. Check the current docs for the exact resizing rules, as of October 2026.

Now consider what a version change can do here. It could switch screenshots from off to on by default. It could change the default viewport. It could start sending a screenshot on every step instead of only when the DOM-based approach fails. It could send two images per step, a raw one and an annotated one. Any of these doubles the image portion without a single line of your code changing. If the default viewport changes from 1280 by 800 to 1920 by 1080, image cost per step roughly doubles by itself, and you would never see a warning.

cache invalidation

This is the subtle one. Suppose the old version put the system prompt and tool schema first, then the stable task description, then the volatile page state last. Prefix caching works well. Now suppose a new version injects something volatile near the top: a current timestamp, a step counter, a random ID, a “current URL” line, or a reordered tool list (for example, built from a Python set or dict with unstable ordering). The prefix now differs on every call. The cached portion that used to be billed at the cache read rate is billed at the full input rate.

Notice what this does to your totals. Prompt size did not change at all. If you compare token counts between versions, they match. Only the billing category moved. This is why you must record cache read and cache write tokens per call, not just total input.

extra calls per step

Some versions add auxiliary model calls: a planner call every N steps, a memory summarization call, a validation call after each action, a “did the task complete” judge call. Each is a full prompt, often including the page state. If the default for one of these flips on, you’ve multiplied calls per step. Often the library lets you set a separate cheaper model for these, but the default may be your main model.

worked examples

These are worked arithmetic examples, not measurements from a specific release. I’m using assumed round numbers so you can see how each mechanism scales, and I’ll say clearly where each number is an assumption. Plug in your own.

Assume a price of $3 per million input tokens for fresh input and a cache read price of one tenth of that, for illustration only. Your provider’s real prices will differ, so check them before relying on any of this.

example 1: viewport default change

Assume an agent runs 25-step tasks with one screenshot per step.

  • before: 1280 by 800 screenshot, about 1,365 tokens per step
  • after: 1920 by 1080 screenshot, about 2,765 tokens per step

The delta is about 1,400 tokens per step, or 35,000 tokens per 25-step task. At $3 per million that is about $0.105 per task extra. Small per task. At 10,000 tasks per month it is $1,050 per month, from a viewport default you never chose. And it is only the image. If the screenshot was the dominant component of your prompt, you’ve nearly doubled that part.

The fix is trivial once you know: pin the viewport explicitly in your browser config rather than inheriting the library default. The audit is what tells you to.

example 2: history retention

Assume a base prompt (system, schema, task) of 6,000 tokens, page state of 3,000 tokens, and each step adds 400 tokens of history.

  • version A drops old results and keeps only the last 3 steps: history stays around 1,200 tokens. Per-step prompt is roughly 10,200 tokens at every step. Over 30 steps that is about 306,000 input tokens.
  • version B keeps all history verbatim: history at step n is 400 times n. Per-step prompt grows from 9,400 at step 1 to 21,000 at step 30. Summing, the history portion alone is 400 times (30 times 31 / 2), which is 186,000 tokens, plus 30 times 9,000 for the fixed part, which is 270,000. Total 456,000.

That is about 1.5 times the input of version A on a 30-step task, and the gap widens on longer tasks, because version B is quadratic in step count. At 60 steps, version B history is 400 times 1,830, which is 732,000, plus 540,000 fixed, so 1,272,000 against version A at 60 times 10,200, which is 612,000. That is past 2x. So a task-length distribution with a long tail can turn “no behavior change” into a doubled bill, and the doubling comes mostly from your longest tasks.

example 3: a cache-breaking timestamp

Assume a 9,000 token stable prefix (system prompt, schema, task) and 3,000 tokens of volatile page state per step, 20 steps per task.

  • cache working: 9,000 tokens read from cache per step at the cache read price (0.3 dollars per million in this illustration), plus 3,000 fresh at 3 dollars per million. Per step: 0.0027 plus 0.009 = 0.0117 dollars. Over 20 steps: about 0.234 dollars. I’m ignoring the cache write premium on the first call, which matters little over 20 steps.
  • cache broken: all 12,000 tokens fresh at 3 dollars per million. Per step: 0.036 dollars. Over 20 steps: 0.72 dollars.

That is about a 3x difference on input cost, with identical token counts. If only the prefix was breaking and you had other uncached costs, you’d see something between 1x and 3x. This is how a “same prompt size, doubled bill” incident looks. If your only monitoring is total tokens, you will miss it completely.

edge cases and failure modes

Auditing for this has its own traps. These are the ones I’ve hit or seen.

  • comparing only totals: if you only compare total tokens per task between versions, you can’t tell prompt growth from step-count growth from call-count growth. Counter: log per call, and break each call into components (system, schema, history, page state, image tokens, cached and uncached input). Then totals decompose.
  • nondeterministic agents: the agent may take a different path on version B, so step counts differ and you can’t compare cleanly. Counter: replay recorded observations through both versions’ prompt builders instead of running live. You want the same inputs, so any difference is attributable to context construction. If the library doesn’t let you call the prompt builder without a live browser, snapshot pages to disk and serve them locally.
  • short test tasks hide quadratic growth: a 5-step smoke test looks identical across versions even when history retention changed. Counter: replay a long recorded trajectory (30 steps or more) taken from production, and plot prompt tokens against step number. A flat line versus a rising line tells you at a glance.
  • cache effects invisible without usage fields: some wrappers drop the cache fields from the response usage object. Counter: read the raw provider response and log cache_read_input_tokens and cache_creation_input_tokens (field names for Anthropic’s API, per their docs). If cache reads drop to zero on the new version, you’ve found it.
  • default changes behind config you never set: if you never set a viewport, vision flag, or max history, you inherit whatever the new default is. Counter: dump the library’s fully resolved config at startup in both versions and diff the dumps. Then pin every value that affects prompt size.
  • auxiliary calls on the wrong model: a new planner or judge call that inherits your main model. Counter: count model calls per step as a first-class metric, and check whether the library allows a cheaper model for secondary calls.

One more point on failure mode. Don’t assume the upgrade is the only variable. If you changed model version, provider routing, or the sites you target in the same window, separate those. Pin the model ID and replay on the same recorded inputs.

an audit method you can run yourself

This is the part I’d keep if you skip everything else. It takes about half a day the first time and about an hour after you’ve scripted it.

  1. record real trajectories. Capture 10 to 20 production runs, including a few long ones. For each step, store the page state the agent saw, the screenshot bytes, and the action it took. You need these on disk so you can replay without a live browser.

  2. install both versions in separate virtualenvs. Pin exact versions of old and new. Don’t test “latest” against “whatever I had”.

  3. dump resolved config in each. Print every default that touches context: viewport size, vision flag, max history items, max elements, DOM attribute inclusion, planner and judge settings. Diff them.

  4. intercept the outgoing request. The reliable way is to put a logging layer at the HTTP or client boundary, not inside the library. Here is a minimal approach for a Python client using a custom httpx transport so it works regardless of version:

import json
import httpx

class LoggingTransport(httpx.HTTPTransport):
    def __init__(self, sink_path, **kw):
        super().__init__(**kw)
        self.sink_path = sink_path

    def handle_request(self, request):
        body = json.loads(request.content or b"{}")
        response = super().handle_request(request)
        response.read()
        usage = {}
        try:
            usage = json.loads(response.content).get("usage", {})
        except Exception:
            pass
        with open(self.sink_path, "a") as f:
            f.write(json.dumps({"request": body, "usage": usage}) + "\n")
        return response

You then pass an httpx.Client(transport=LoggingTransport("calls.jsonl")) into your model client, if the library lets you inject one. If it doesn’t, a local proxy such as mitmproxy does the same job at the network level. Be careful that these logs contain page content and possibly user data, so keep them on your own machine and scrub them before sharing.

  1. decompose every logged request. Write a short script that, for each call, counts characters or tokens of the system block, tool schema, each message, and image blocks. Use the provider’s token counting endpoint if you want exact numbers rather than estimates. Emit one row per call: step number, component, tokens.

  2. check prefix stability. For each pair of consecutive calls in a run, compute the longest common prefix of the serialized request. Compare it to the total length. In the good version the common prefix should cover the system prompt, schema and task. In a cache-broken version it will stop early, and the first differing character will tell you what’s injecting volatility. This one check finds timestamp and ordering bugs in seconds.

  3. plot three charts per version. Prompt tokens against step number, calls per step, and cache read ratio against step number. A regression almost always shows in one of these three as an obvious shape change.

  4. bisect if needed. If the two versions are many releases apart, bisect between them. Because your replay harness is deterministic, bisecting is mechanical.

  5. add a regression gate. Once you have the harness, run it in CI on every dependency bump with a threshold on tokens per step and on cache read ratio. Fail the bump if either moves past your tolerance. This turns a surprise on the invoice into a failing check.

That is the direct answer to the question in the issue. If your spend jumped after an upgrade, the cause is in one of four places (prompt size, step count, cache behavior, calls per step), and the harness above tells you which in under a day. For the specific thread, read the maintainers’ replies and the changelog between your two versions, then confirm against your own replay. Don’t take my word, or anyone’s, that it is the same cause you have.

what we learned in production

The biggest lesson for me was about where to put the instrumentation. We used to log token totals at the end of a task, because that is what the dashboard showed. That told us the bill, never the reason. Moving to per-call logging with component breakdowns felt like overkill until the first regression, after which it paid for itself in one afternoon. If you only add one thing, add cache read tokens and calls per step to your per-task metrics. Tracing tools help here, and I compared several in best tools for tracing and debugging AI agents. Whatever you pick, make sure it records raw provider usage fields rather than a normalized total.

The second lesson is to pin things you didn’t think were yours. Viewport, vision on or off, history length, model ID, and auxiliary model settings should all live in your own config with explicit values. Long-lived sessions make this more important, since a default drift mid-fleet is hard to spot; the patterns in session lifecycles for long running agents apply to config as much as to cookies. I also stopped upgrading browser automation dependencies on a schedule. They go through the replay gate first, and I read the changelog with the specific question “what does this change about what the model sees”. For more on the posts we publish in this area, the blog index is the place to start.

References below are the sources I relied on. I did not run the tooling against the specific releases in the issue, so treat the worked examples as arithmetic you should rerun with your own numbers.

references and further reading

Written by Xavier Fok

disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-22.

free download
Why did my agent get blocked? A triage checklist

The checks we run, in order, when a browser or phone agent starts failing: network, fingerprint, behaviour, account. Leave your email and we will also tell you when we publish a new field note, a few times a month at most.

from the team behind this site
A Singapore mobile IP for browser agents

Singapore Mobile Proxy runs real mobile IPs on SingTel, StarHub and M1, with sticky sessions so one task keeps one IP. Singapore only: a fit for SEA or location-agnostic work, the wrong tool if you need a US IP.

see plans →
from the team behind this site
A real Android phone for phone-use agents

cloudf.one hosts real Android phones in Singapore on dedicated hardware, each with a persistent Singapore mobile IP. For agents that need an actual device and a stable carrier identity.

get a phone →
read on
More from The Agent Ops Report

Blocks, sessions, retries, traces, cost per task and phone-use agents. Browse all articles →