You wired Stagehand into a workflow, pointed it at Browserbase, and one of two things sent you here: the token spend climbed faster than expected, or you enabled caching, ran the exact same call twice, and got cacheStatus: "MISS" both times. Both problems trace back to defaults that are tuned for accuracy, not for your bill.
Stagehand cost has two buckets: LLM tokens per act/extract/agent step, and Browserbase session hours. The biggest levers are model choice and capping agent maxSteps. On caching: server-side entries only start serving after a hit-count threshold, typically around 100 hits, so low-volume workloads see MISS forever. That behavior lives in one GitHub issue comment, nowhere in the docs.
Everything below is against Stagehand 3.x (3.6.0 is current as of mid-2026), TypeScript SDK, with claims sourced to the docs, the Browserbase engineering blog, or specific GitHub issues.
The silent serverCache failure
Start with the trap, because it is the one nobody warns you about. In February 2026, issue #1767 reported that server-side caching was not working for extract(), act(), and observe(). The repro was as clean as it gets: a default npx create-browser-app project with env: "BROWSERBASE" and serverCache: true, calling stagehand.extract("Extract the value proposition from the page.") twice against the same page. Both calls logged extract server cache miss, both returned cachedInputTokens: 0 and cacheStatus: "MISS". Another user confirmed the same behavior on 3.4.0 three months later. Curiously, agent() calls did cache for the original reporter.
The resolution, from a contributor in June 2026, is the part that should be printed in bold on the caching docs page: server-side caching has a hit count threshold that must be exceeded before a cached entry is served. It is typically set to 100, configurable per project, and changing it means emailing [email protected]. That comment is the only place this is written down.
Combine that with the other documented property of the server cache, a 48-hour TTL, and you get an uncomfortable corollary: if a given cache entry does not accumulate roughly 100 identical hits within 48 hours, it expires before it ever serves. A nightly job that runs the same extract once a day will be a cache miss every single night, forever, by design. The cache is built for high-frequency production traffic, and if that is not your traffic shape, serverCache: true is doing nothing for your act/extract/observe calls no matter how carefully you write your instructions.
So before you spend an afternoon debugging instruction wording, ask the volume question first: does this exact call run 100 or more times in a two-day window? If not, the server cache is not your tool. The local cache, covered below, may be.
What actually drives the token bill
Stagehand's cost-optimization docs split spend into two buckets: LLM inference and browser infrastructure. The inference bucket is where the surprises live, and the three primitives are not equally priced.
act() is the cheapest call: one instruction, one action resolution. extract() ships page content to the model along with your schema, so its input tokens scale with the page. agent() is the multiplier: every step in the loop is an LLM call, and maxSteps defaults to 20. An agent that wanders costs you 20 model calls before it gives up, each one re-sending context. One Hacker News commenter described exactly this failure mode in a May 2026 thread: token costs "that ate the budget" on non-trivial tasks because large page portions get re-read on every step. Another described setting a daily spend cap after burning $40 on a prompt loop.
For actual numbers, the most detailed public accounting is Steven Gonsalvez's Framework Wars comparison on dev.to (April 2026, updated July). By his measurements: Stagehand runs around 2,000 to 5,000 tokens on a first run and near zero when cached, working out to $0.01 to $0.10 per task. Browser-use came in at 7,000 to 15,000 tokens per ten steps ($0.02 to $0.30 per task), and Skyvern, which sends screenshots, at 30,000 to 50,000 tokens ($0.10 to $0.50 per task). Those are his figures, not benchmarks we have reproduced, but the relative ordering matches how the architectures work. His framing line holds for every one of these tools: every step in the agent loop is an LLM call.
The infrastructure bucket is simpler. Browserbase pricing as of July 2026: the free tier gives you 3 concurrent browsers and 1 browser hour. Developer is $20/month for 25 concurrent and 100 hours, with overage at $0.12/hour. Startup is $99/month for 500 hours at $0.10/hour overage. Sessions default to a one-hour timeout per the docs, so a script that crashes without closing its session keeps billing browser time until the timeout fires.
The cost levers, in escalating order
The docs give you a toolkit. Applied in order of effort:
Model choice first. Stagehand's docs recommend using models from least to most expensive based on task complexity, with google/gemini-2.5-flash as the recommended default and stagehand.dev/evals as the accuracy reference. Set it in the constructor, override per call for the hard cases.
stagehand.config.ts
import { Stagehand } from "@browserbasehq/stagehand";
const stagehand = new Stagehand({
env: "BROWSERBASE",
model: "google/gemini-2.5-flash",
browserbaseSessionCreateParams: {
timeout: 1800, // 30 minutes instead of the default 1 hour
keepAlive: true,
},
});Cheap-model-first fallback. The docs sketch a smartAct pattern: try the instruction with a cheap model, catch failures, escalate to a stronger one. You pay the expensive model only on the calls where the cheap one actually fails, which for well-scoped act instructions is a minority.
Cap agent steps and read the usage. If a test should finish in 6 steps, do not leave maxSteps at its default of 20. The AgentResult reports token usage per run, and stagehand.metrics accumulates totals across the session:
run-test.ts
const agent = stagehand.agent({ mode: "dom", model: "google/gemini-2.5-flash" });
const result = await agent.execute({
instruction: "Log in and open the billing page",
maxSteps: 8,
});
console.log(result.completed, result.usage.input_tokens, result.usage.output_tokens);
console.log(stagehand.metrics.totalPromptTokens, stagehand.metrics.totalCompletionTokens);Session pooling. Session creation and teardown is billed browser time. The docs describe a SessionManager keeping a Map of Stagehand instances keyed by task type, reusing warm sessions instead of creating one per task.
A hard budget stop. The docs also document a BudgetGuard class, defaulting to a $25 daily budget, whose checkBudget throws a Daily budget exceeded error before a call runs. After the $40 prompt-loop anecdote above, treat this as insurance, not paranoia. A runaway agent loop is a matter of when.
Two caches, and most articles conflate them
Stagehand has two distinct caching layers, and misdiagnosing which one you are using wastes debugging time.
The server cache (serverCache) is Browserbase-hosted, enabled by default when env: "BROWSERBASE", scoped to your Browserbase Project ID, has the 48-hour TTL, and is subject to the hit-count threshold from issue #1767. Project scoping matters more than it sounds: if your CI and production run under separate Browserbase projects, they do not share cache entries, and each project has to cross the threshold independently. You can disable it globally in the constructor with serverCache: false or per call in the options.
The local cache (cacheDir in the constructor, for example cacheDir: "act-cache") writes to your filesystem, persists across runs with no documented TTL, and applies to act() and agent() per the docs. Because it is just files, you can commit it to your repo and share it across CI runs, which sidesteps the threshold problem entirely. Clear a stale entry with rmSync("act-cache", { recursive: true, force: true }).
Under the hood, per the Browserbase caching blog post, the server cache key is a SHA-256 hash over a canonical string of the method, normalized URL, a DOM hash, the project scope, and method-specific fields, with nested objects like schemas and model config hashed separately. The design, in their words, prioritizes accuracy over hit rates, with speedups as high as roughly 80% between sequential runs by their account. That accuracy-first design is exactly why the cache is so easy to miss, which brings us to the taxonomy.
Why your cache keeps missing
The caching docs state that the cache key is generated from the instruction, the page content, and the options you pass. Each component is an invalidation surface.
Instruction wording, down to punctuation. The docs are explicit that even minor wording changes, including synonyms, extra adjectives, and punctuation, produce a new key and a miss. The killer version of this is interpolating data into the instruction string. `type ${email} into the Email field` produces a different key for every email address. The fix is variables, where the key uses the variable names, not the values:
login.ts
// One canonical instruction constant. Never interpolate values into it.
const FILL_EMAIL = "type %email% into the Email address field";
await stagehand.act(FILL_EMAIL, {
variables: { email: "[email protected]" },
});The docs put it plainly: with variables you prime the cache once and hit it forever.
Dynamic URLs. The page URL factors into the key. Browserbase filters out some query parameters like referral trackers, but by their own admission they do not catch everything yet. Session IDs, timestamps, or per-user slugs in the URL mean a fresh key per visit.
Viewport, user agent, locale. All part of page content as the cache sees it. Lock them:
setup.ts
const page = stagehand.context.pages()[0];
await page.setViewportSize({ width: 1280, height: 720 });Fix the user agent and locale in your launch configuration too, so a CI runner and your laptop produce the same key.
Third-party DOM noise. Analytics snippets, A/B testing frameworks, and ad slots mutate the DOM between visits, which changes the DOM hash. Route-block those domains in test environments.
Verify it, do not assume it
act and extract results carry a cacheStatus field when the server cache is active. Log it every run and you stop guessing:
verify-cache.ts
await page.goto("https://example.com/login");
const first = await stagehand.act("click the login button");
console.log(first.cacheStatus); // "MISS"
await page.goto("https://example.com/login");
const second = await stagehand.act("click the login button");
console.log(second.cacheStatus); // "HIT" only past the hit-count thresholdThe working checklist: log cacheStatus on every act/extract call, watch cachedInputTokens in your LLM usage, and compare stagehand.metrics.totalPromptTokens across two identical runs. If tokens do not drop on the second run, nothing cached, whatever your config says. And if you see MISS on genuinely identical calls, revisit the volume question from the top before touching your instructions.
Where Smoketest fits
Everything above works, and if you are building a scraping pipeline or an agent product, it is worth doing. If what you are actually building is post-deploy checks of your own app, the calculus changes: you are now hand-tuning cache keys, model fallbacks, and budget guards for tests that run a handful of times a day, exactly the frequency where server caching mathematically cannot help. Smoketest runs described-in-words browser tests as a managed service, so the token economics and cache plumbing are our problem instead of a sidecar codebase you maintain.
FAQ
Why is my Stagehand cache not working?
Most likely you have not crossed the server cache's hit-count threshold, typically around 100 hits per entry, per issue #1767. Below that, act/extract/observe calls return MISS by design. Other causes: instruction wording changes, dynamic URL parameters, differing viewport or locale, or third-party scripts mutating the DOM between visits.
How much does Stagehand cost per action?
There is no official per-action price. The most cited independent figures, from dev.to's Framework Wars comparison, put Stagehand at roughly 2,000 to 5,000 tokens on a first run, or $0.01 to $0.10 per task, dropping near zero when cached. Add Browserbase session time, from $0.10 to $0.12 per browser hour in overage.
Does serverCache work with act and extract?
Yes, but only after an entry exceeds the per-project hit-count threshold, and entries expire after 48 hours. Low-frequency calls never qualify. Verify with the cacheStatus field on act and extract results. For low-volume workloads, use the local cacheDir cache instead, which persists on disk and covers act() and agent().
How do I make Stagehand cheaper?
In order of impact: use a cheap default model like google/gemini-2.5-flash and escalate only on failure, lower agent maxSteps from its default of 20, reuse Browserbase sessions instead of creating one per task, shorten the session timeout from the one-hour default, and wrap spend in a budget guard that throws before a runaway loop finishes.
What invalidates the Stagehand cache key?
Per the caching docs, the key is built from the instruction, page content, and options. Any wording or punctuation change, values interpolated into instruction strings, dynamic URL parameters, viewport or user agent or locale differences, and DOM changes from analytics or A/B scripts all produce new keys. Use variables for data, lock the environment, block trackers.


