Skip to main content
Stagehand supports two caching strategies to reduce LLM costs and speed up your automations: Browserbase Cache and Local Cache. They serve different use cases and can be used independently or together.

Browserbase Cache

Browserbase Cache is a managed, server-side caching layer built into the Stagehand API. When you run Stagehand with env: "BROWSERBASE", every act() call is automatically cached on Browserbase’s servers. Repeated calls with the same inputs return instantly without consuming any LLM tokens. You don’t need to configure anything to start benefiting. The cache key is generated from the instruction, page content, and options you pass. On a cache hit, the response is returned directly from the server with no LLM inference and no token cost. You can inspect cache behavior via the cacheStatus field returned by act(). Check out the Browserbase blog for more details on how it works under the hood.

Disabling on the Constructor

Pass serverCache: false to disable caching for all requests made by that instance:
import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({
  env: "BROWSERBASE",
  serverCache: false,
});

await stagehand.init();
const page = stagehand.context.pages()[0];

await page.goto("https://example.com");

// Cache is disabled, always hits the LLM
await stagehand.act("click the login button");

Disabling per Call

Override the instance setting for a single call by passing serverCache: false in the options:
import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({ env: "BROWSERBASE" }); // caching on by default

await stagehand.init();
const page = stagehand.context.pages()[0];
await page.goto("https://example.com");

// This call skips the cache
await stagehand.act("click the login button", { serverCache: false });

// This call uses the cache as normal
await stagehand.act("submit the form");

Inspecting Cache Status

act() returns a cacheStatus field you can use to verify whether a result was served from cache:
const actResult = await stagehand.act("click the login button");
console.log(actResult.cacheStatus); // "HIT" or "MISS"

Limitations

  • The page URL factors in to the cache key. If the action is being made on a page with a dynamic URL, caching may not work as expected. We do filter out certain query parameters like referral trackers and analytics, but we don’t catch everything just yet.
  • If the page content or structure changes, the action won’t get a cache HIT and the LLM will be called. The subsequent actions will attempt to hit the resulting cache entry.

Best Practices

If you call a Stagehand method immediately after page.goto(), the page content may still be streaming in. This means the accessibility tree captured for the cache key is shorter than what the page will eventually render — so subsequent calls on the fully-loaded page will produce a different hash and miss the cache. Call page.waitForLoadState("networkidle") first to ensure the full tree is stable before Stagehand snapshots it.
async function example(stagehand: Stagehand) {
  const page = stagehand.context.pages()[0];
  await page.goto("https://www.google.com/search?q=browserbase");

  await page.waitForLoadState("networkidle");

  // All content is loaded — consistent hash across runs
  const result1 = await stagehand.observe("click the first search result");
  // MISS

  const result2 = await stagehand.observe("click the first search result");
  // HIT
}
When targeting a specific part of a page, pass a selector to scope the accessibility tree snapshot to that container. This reduces token costs, speeds up inference, and — crucially for caching — means that changes outside the container don’t affect the cache key.
async function example(stagehand: Stagehand) {
  const page = stagehand.context.pages()[0];
  await page.goto("https://www.google.com/search?q=browserbase");

  await page.waitForLoadState("networkidle");

  const result = await stagehand.observe("click the first search result", {
    // Scope to the search results container so ads, headers,
    // and other surrounding content don't pollute the cache key.
    selector: "#rcnt",
  });
}
Using variables in act() lets you generalize a single cache entry across many different values. The cache key is built from the variable keys, not the values — so { email: "alice@example.com" } and { email: "bob@example.com" } share the same entry.Without variables you’d accumulate a separate cache miss for every unique email address you type. With variables you prime the cache once and hit it forever.
async function example(stagehand: Stagehand) {
  const page = stagehand.context.pages()[0];
  await page.goto("https://example.com/login");

  await page.waitForLoadState("networkidle");

  // First run with alice@example.com → MISS (primes cache)
  // Any future run, regardless of email value → HIT
  await stagehand.act(
    "type %email% into the Email address field",
    { variables: { email: "alice@example.com" } },
  );
}
Small differences in runtime state produce different accessibility trees and therefore different cache keys. Keep your environment as deterministic as possible:
  • Fixed viewport sizeawait page.setViewportSize({ width: 1280, height: 720 })
  • Consistent user agent and locale — set these in your browser launch options if your stack supports it
  • Block noisy third-party requests — analytics, A/B testing scripts, and ad trackers can inject DOM nodes that shift the cache key on every load
const page = stagehand.context.pages()[0];

// Lock the viewport so the layout is identical across runs
await page.setViewportSize({ width: 1280, height: 720 });

// Block third-party noise that pollutes the accessibility tree
await page.route("**/*.{png,jpg,gif,svg,woff,woff2}", (route) => route.abort());
The instruction string is part of the cache key. Even minor wording changes — synonyms, extra adjectives, punctuation — produce a new key and a cache miss.
  • Anchor instructions to visible UI labels: "click the Sign in button" not "click the button to log me in"
  • Keep instructions short and free of filler words
  • Avoid instructions that contain runtime-variable text inline — use variables instead
// Good: anchored to the visible label, no variance
await stagehand.act("click the Sign in button");

// Bad: inline dynamic value creates a new cache key every time
await stagehand.act(`type ${email} into the Email address field`);

Local Cache

Local Cache writes action results to your filesystem so they persist across script runs. It works in both LOCAL and BROWSERBASE environments. When you specify a cacheDir, Stagehand saves every action and agent step to a local file on first run, then replays those cached actions on subsequent runs with no LLM calls, no token cost, and no network round-trip to Browserbase. This is especially useful for:
  • CI/CD pipelines - commit your cache directory to version control for consistent, deterministic runs across environments
  • Local development - iterate on automations without burning tokens on repeated runs
  • Cross-machine sharing - cache files are portable and can be shared across machines

Caching with act()

Cache actions from act() by specifying a cache directory in your Stagehand constructor.
import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({
  env: "BROWSERBASE",
  cacheDir: "act-cache", // Specify a cache directory
});

await stagehand.init();
const page = stagehand.context.pages()[0];

await page.goto("https://browserbase.github.io/stagehand-eval-sites/sites/iframe-same-proc-scroll/");

// First run: uses LLM inference and caches
// Subsequent runs: reuses cached action
await stagehand.act("scroll to the bottom of the iframe");

// Variables work with caching too
await stagehand.act("fill the username field with %username%", {
  variables: {
    username: "fakeUsername",
  },
});

Caching with agent()

Cache agent actions (including Computer Use Agent actions) the same way. Just specify a cacheDir. The cache key is automatically generated based on the instruction, start URL, agent execution options, and agent configuration. Subsequent runs with the same parameters will reuse cached actions.
import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({
  env: "BROWSERBASE",
  cacheDir: "agent-cache", // Specify a cache directory
});

await stagehand.init();
const page = stagehand.context.pages()[0];

await page.goto("https://browserbase.github.io/stagehand-eval-sites/sites/drag-drop/");

const agent = stagehand.agent({
  mode: "cua",
  model: "google/gemini-3-flash-preview",
  systemPrompt: "You are a helpful assistant that can use a web browser.",
});

await page.goto("https://play2048.co/");

// First run: uses LLM inference and caches
// Subsequent runs: reuses cached actions
const result = await agent.execute({
  instruction: "play a game of 2048",
  maxSteps: 20,
});

console.log(JSON.stringify(result, null, 2));

Cache Directory Organization

You can organize your caches by using different directory names for different workflows:
// Separate caches for different parts of your automation
const loginStagehand = new Stagehand({
  env: "BROWSERBASE",
  cacheDir: "cache/login-flow"
});

const checkoutStagehand = new Stagehand({
  env: "BROWSERBASE",
  cacheDir: "cache/checkout-flow"
});

const dataExtractionStagehand = new Stagehand({
  env: "BROWSERBASE",
  cacheDir: "cache/data-extraction"
});

Best Practices

Organize caches by workflow or feature for easier management:
// Good: descriptive cache names
cacheDir: "cache/login-actions"
cacheDir: "cache/search-actions"
cacheDir: "cache/form-submissions"

// Avoid: generic cache names
cacheDir: "cache"
cacheDir: "my-cache"
If the website structure changes significantly, clear your cache directory to force fresh inference:
rm -rf cache/login-actions
Or programmatically:
import { rmSync } from 'fs';

// Clear cache before running if needed
if (shouldClearCache) {
  rmSync('cache/login-actions', { recursive: true, force: true });
}

const stagehand = new Stagehand({
  env: "BROWSERBASE",
  cacheDir: "cache/login-actions"
});
Consider committing your cache directory to version control for consistent behavior across environments:
# .gitignore
# Don't ignore cache directories
!cache/
This ensures your CI/CD pipelines use the same cached actions without needing to run inference on first execution.