Overview
Deploying Stagehand to production requires careful consideration of infrastructure, error handling, monitoring, and cost management. This guide covers deployment strategies and best practices.Environment Configuration
Use Browserbase for Production
const stagehand = new Stagehand({
env: "BROWSERBASE",
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
browserbaseSessionCreateParams: {
browserSettings: {
blockAds: true,
solveCaptchas: true,
},
proxies: true,
},
});
- Managed browser infrastructure
- Automatic scaling
- Built-in proxy support
- CAPTCHA solving
- Session recording for debugging
- No browser maintenance overhead
Local Environment (Development)
const stagehand = new Stagehand({
env: "LOCAL",
localBrowserLaunchOptions: {
headless: true,
},
});
- Development and testing
- Cost-sensitive workflows
- Self-hosted infrastructure
Environment Variables
Required Variables
# .env.production
BROWERBASE_API_KEY=your_api_key
BROWERBASE_PROJECT_ID=your_project_id
# LLM Provider (choose one)
OPENAI_API_KEY=your_openai_key
ANTHROPIC_API_KEY=your_anthropic_key
# Optional
STAGEHAND_CACHE_DIR=/app/cache
STAGEHAND_VERBOSE=0
STAGEHAND_TIMEOUT_MS=30000
Load Configuration
import dotenv from "dotenv";
// Load environment-specific config
if (process.env.NODE_ENV === "production") {
dotenv.config({ path: ".env.production" });
} else {
dotenv.config({ path: ".env.development" });
}
const stagehand = new Stagehand({
env: process.env.STAGEHAND_ENV as "LOCAL" | "BROWSERBASE",
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
model: process.env.STAGEHAND_MODEL,
verbose: parseInt(process.env.STAGEHAND_VERBOSE || "0") as 0 | 1 | 2,
cacheDir: process.env.STAGEHAND_CACHE_DIR,
});
Caching Strategy
Deploy with Pre-Warmed Cache
// scripts/warmCache.ts
import { Stagehand } from "@browserbasehq/stagehand";
async function warmCache() {
const stagehand = new Stagehand({
env: "LOCAL",
cacheDir: "./production-cache",
});
await stagehand.init();
const page = stagehand.context.pages()[0];
// Run all production workflows once
await page.goto("https://your-app.com/login");
await stagehand.act("click the login button");
await stagehand.act("type '[email protected]' in email field");
await page.goto("https://your-app.com/products");
await stagehand.extract("get all product prices");
// Add more workflows...
await stagehand.close();
console.log("Cache warmed successfully");
}
warmCache();
# Warm cache locally
npm run warm-cache
# Include cache in Docker image or deploy
Cache Persistence
# Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# Include pre-warmed cache
COPY ./production-cache /app/cache
ENV STAGEHAND_CACHE_DIR=/app/cache
CMD ["node", "dist/index.js"]
Error Handling
Comprehensive Error Handling
import {
ActTimeoutError,
StagehandError,
CdpConnectionClosedError,
} from "@browserbasehq/stagehand";
import * as Sentry from "@sentry/node";
async function executeWorkflow(instruction: string) {
const stagehand = new Stagehand({
env: "BROWSERBASE",
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
});
try {
await stagehand.init();
const result = await stagehand.act(instruction);
return result;
} catch (error) {
// Log to monitoring service
Sentry.captureException(error, {
tags: {
operation: "stagehand-workflow",
instruction,
},
});
// Handle specific errors
if (error instanceof ActTimeoutError) {
throw new Error("Operation timed out");
} else if (error instanceof CdpConnectionClosedError) {
throw new Error("Browser connection lost");
} else if (error instanceof StagehandError) {
throw new Error(`Stagehand error: ${error.message}`);
}
throw error;
} finally {
await stagehand.close();
}
}
Retry Logic
async function executeWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
delayMs = 1000
): Promise<T> {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1}/${maxRetries} failed, retrying...`);
if (i < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs * (i + 1)));
}
}
}
throw lastError;
}
// Usage
const result = await executeWithRetry(async () => {
const stagehand = new Stagehand({ env: "BROWSERBASE" });
await stagehand.init();
const result = await stagehand.act("click button");
await stagehand.close();
return result;
});
Monitoring and Logging
Structured Logging
import winston from "winston";
const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" }),
],
});
const stagehand = new Stagehand({
env: "BROWSERBASE",
verbose: 1,
logger: (line) => {
logger.log({
level: line.level === 0 ? "error" : "info",
category: line.category,
message: line.message,
...line.auxiliary,
});
},
});
Metrics Collection
import { Counter, Histogram } from "prom-client";
const actCounter = new Counter({
name: "stagehand_act_total",
help: "Total number of act() calls",
labelNames: ["status"],
});
const actDuration = new Histogram({
name: "stagehand_act_duration_seconds",
help: "Duration of act() calls",
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
async function monitoredAct(stagehand: Stagehand, instruction: string) {
const start = Date.now();
try {
const result = await stagehand.act(instruction);
actCounter.inc({ status: "success" });
actDuration.observe((Date.now() - start) / 1000);
return result;
} catch (error) {
actCounter.inc({ status: "error" });
throw error;
}
}
Browserbase Session Tracking
const stagehand = new Stagehand({
env: "BROWSERBASE",
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
});
await stagehand.init();
// Log session info for debugging
console.log("Session ID:", stagehand.browserbaseSessionID);
console.log("Session URL:", stagehand.browserbaseSessionURL);
console.log("Debug URL:", stagehand.browserbaseDebugURL);
// Store session ID for later replay
const sessionId = stagehand.browserbaseSessionID;
// Later, reconnect to the same session
const reconnected = new Stagehand({
env: "BROWSERBASE",
browserbaseSessionID: sessionId,
});
Scaling Strategies
Worker Queue Pattern
import { Queue, Worker } from "bullmq";
const taskQueue = new Queue("stagehand-tasks", {
connection: {
host: "redis",
port: 6379,
},
});
// Add tasks to queue
await taskQueue.add("scrape", {
url: "https://example.com",
instruction: "extract product data",
});
// Worker processes tasks
const worker = new Worker(
"stagehand-tasks",
async (job) => {
const stagehand = new Stagehand({ env: "BROWSERBASE" });
await stagehand.init();
try {
const page = stagehand.context.pages()[0];
await page.goto(job.data.url);
const result = await stagehand.extract(job.data.instruction);
return result;
} finally {
await stagehand.close();
}
},
{
connection: {
host: "redis",
port: 6379,
},
concurrency: 5, // Process 5 jobs in parallel
}
);
Serverless Deployment
// AWS Lambda handler
import { Stagehand } from "@browserbasehq/stagehand";
export const handler = async (event: any) => {
const stagehand = new Stagehand({
env: "BROWSERBASE", // Always use Browserbase for serverless
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
cacheDir: "/tmp/stagehand-cache", // Lambda /tmp storage
});
try {
await stagehand.init();
const page = stagehand.context.pages()[0];
await page.goto(event.url);
const result = await stagehand.extract(event.instruction);
return {
statusCode: 200,
body: JSON.stringify(result.extraction),
};
} catch (error) {
console.error("Error:", error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
} finally {
await stagehand.close();
}
};
service: stagehand-service
provider:
name: aws
runtime: nodejs18.x
timeout: 300 # 5 minutes
memorySize: 2048
environment:
BROWSERBASE_API_KEY: ${env:BROWSERBASE_API_KEY}
BROWSERBASE_PROJECT_ID: ${env:BROWSERBASE_PROJECT_ID}
functions:
scrape:
handler: handler.handler
events:
- http:
path: scrape
method: post
Container Deployment
# Dockerfile
FROM node:18-slim
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --production
# Copy application
COPY . .
# Copy pre-warmed cache
COPY ./production-cache /app/cache
# Environment
ENV NODE_ENV=production
ENV STAGEHAND_CACHE_DIR=/app/cache
# Health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD node healthcheck.js
CMD ["node", "dist/index.js"]
version: "3.8"
services:
stagehand-worker:
build: .
environment:
- BROWSERBASE_API_KEY=${BROWSERBASE_API_KEY}
- BROWSERBASE_PROJECT_ID=${BROWSERBASE_PROJECT_ID}
- REDIS_URL=redis://redis:6379
depends_on:
- redis
deploy:
replicas: 3
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Cost Optimization
Session Reuse
const stagehand = new Stagehand({
env: "BROWSERBASE",
keepAlive: true, // Keep session alive
});
await stagehand.init();
// Reuse for multiple operations
for (const task of tasks) {
await executeTask(stagehand, task);
}
// Close when done
await stagehand.close();
Batch Processing
// Process multiple tasks in one session
async function batchProcess(tasks: Task[]) {
const stagehand = new Stagehand({ env: "BROWSERBASE" });
await stagehand.init();
const results = [];
for (const task of tasks) {
const page = stagehand.context.pages()[0];
await page.goto(task.url);
const result = await stagehand.extract(task.instruction);
results.push(result);
}
await stagehand.close();
return results;
}
Security
Secure Credentials
// Use environment variables, never hardcode
const stagehand = new Stagehand({
env: "BROWSERBASE",
apiKey: process.env.BROWSERBASE_API_KEY, // ✅
projectId: process.env.BROWSERBASE_PROJECT_ID, // ✅
// apiKey: "bb_live_...", // ❌ Never hardcode
});
Secrets Management
import { SecretsManager } from "@aws-sdk/client-secrets-manager";
const secretsManager = new SecretsManager({ region: "us-east-1" });
const secret = await secretsManager.getSecretValue({
SecretId: "stagehand/production",
});
const credentials = JSON.parse(secret.SecretString!);
const stagehand = new Stagehand({
env: "BROWSERBASE",
apiKey: credentials.browserbase_api_key,
projectId: credentials.browserbase_project_id,
});
Health Checks
// healthcheck.js
import { Stagehand } from "@browserbasehq/stagehand";
async function healthCheck() {
try {
const stagehand = new Stagehand({
env: "BROWSERBASE",
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
});
await stagehand.init();
const page = stagehand.context.pages()[0];
await page.goto("https://example.com");
await stagehand.close();
console.log("Health check passed");
process.exit(0);
} catch (error) {
console.error("Health check failed:", error);
process.exit(1);
}
}
healthCheck();
Testing in Production
Canary Deployments
const useNewVersion = Math.random() < 0.1; // 10% traffic
const stagehand = new Stagehand({
env: "BROWSERBASE",
model: useNewVersion ? "openai/gpt-4.1" : "openai/gpt-4.1-mini",
});
Feature Flags
import LaunchDarkly from "launchdarkly-node-server-sdk";
const ldClient = LaunchDarkly.init(process.env.LAUNCH_DARKLY_KEY!);
await ldClient.waitForInitialization();
const useCaching = await ldClient.variation(
"stagehand-caching-enabled",
{ key: "user-id" },
false
);
const stagehand = new Stagehand({
env: "BROWSERBASE",
cacheDir: useCaching ? "./cache" : undefined,
});