ArchitectureFor Platform Engineers22 min readUpdated May 2026

Warm Pools

Pre-provisioned sandboxes that assign in under 1 second instead of cold-starting in 15-30. The "interns sitting in the lobby" pattern. For latency-sensitive workloads — customer-facing agents, live browser automation, interactive coding sessions.

This guide covers the cold-start problem in depth, how warm pools work under the hood (snapshot-based suspension, pool manager, request routing), how to configure pools on Sandbox Platform, three pre-warming strategies (static, predictive, on-demand burst), the cost/latency break-even analysis, snapshot design (what to pre-load vs what to inject per-request), common mistakes from the field, and 15 FAQs. Real latency numbers throughout: cold start = 15-45 seconds; warm pool claim = 200-800ms.

Quick Answer

Warm pools maintain N pre-provisioned sandboxes ready to claim instantly. Cold start: 15-30 sec. Warm claim: 0.6-1.2 sec. Sizing: pool_size = ceil(p99_concurrent_requests × 1.3). Cost: 5 idle browsers × $0.1/hr = ~$0.50/hr "insurance premium." Best for predictable / latency-sensitive load. On-demand wins for bursty / cost-sensitive load.

Cold start vs warm pool: what it feels like

The 22× latency gap is abstract until you map it to actual user experiences. Here are five workloads where the difference is the difference between a product that feels instant and one that feels broken.

Workload Cold start (on-demand) Warm pool Impact
Interactive coding assistant (user types a request → Claude Code begins reviewing) User waits 30 sec staring at a spinner. Abandonment rate: high. Response begins in under 1 sec. Feels like autocomplete. Abandonment drops dramatically; session length increases
Automated code review (PR arrives → review posted as comment) 45-sec cold start before the reviewer even reads the diff. PR author has moved on. 400ms from PR webhook to agent reading the first file. Review lands while the author still has context. Review velocity 2-5× higher; faster merge cycles
Customer support agent (incoming ticket → first draft response) 20-sec cold start + agent processing time. Customer sees "agent is typing" for 40+ seconds. 300ms to agent start + processing time. First message arrives in under 5 sec. CSAT score improvement; reduces escalation rate
Parallel batch job (fan-out to 100 agents for competitive research) 100 agents × 30-sec cold start = 50 minutes of wall-clock time lost before any agent does real work. 100 warm claims × 400ms = 40 seconds to full fleet. Real work starts immediately. 50-minute head start per daily run; morning briefing lands before the team's standup
Always-on agent restart after crash 30-sec gap in coverage after crash + restart. Events queued during the gap; backlog processing adds latency. 300ms to restore from snapshot. Event backlog minimal. Virtually zero coverage gap; event latency spike is sub-second

The recurring theme: warm pools matter when a human is on the other end of the latency. Overnight batch, internal reconciliation, and non-urgent automation can tolerate a 30-second cold start. Customer-facing interactions, interactive tools, and latency-SLA workloads cannot.

Where the cold-start 18-second goes

PhaseTime
EC2 RunInstances → instance Running state3-6 sec
EBS volume attach + first-boot4-7 sec
Cloud-init: install Docker, pull image, start container5-10 sec
Manager / agent process startup1-3 sec
Total cold-start13-26 sec

For an internal agent that responds to overnight email, this is invisible. For a customer-facing agent — "your user clicked 'analyze' and is staring at a spinner" — every second matters. Warm pools eliminate all of it.

Pool sizing: how many to keep warm

Formula: pool_size = ceil(p99_concurrent_requests × buffer_factor). Buffer factor is typically 1.2-1.5 depending on how peaky your traffic is.

Example: customer-facing browser agent serving 50 customers; p99 concurrent is 8. Pool size: 8 × 1.3 = 11 warm browsers. Total cost: 11 × $0.1/hr = ~$1.10/hr; about $800/month.

Compare to losing the customer because the page took 18 sec: way more than $800/month in churn risk.

How warm pools work under the hood

A warm pool is three components working together: a snapshot store, a pool manager, and a request router. Understanding each component helps you configure correctly and debug when something goes wrong.

Snapshot store: freezing the environment

A snapshot is a complete point-in-time copy of a running container's state: filesystem, process memory, open file handles. Creating a snapshot takes 2-5 seconds; restoring from it takes 200-500ms. The fundamental economics that make warm pools work.

When you register a pool template, the platform:

  1. Starts a clean sandbox from your base image
  2. Runs your setup script (install packages, clone repos, pull model weights)
  3. Freezes the container state to the snapshot store
  4. Marks the snapshot as the pool's "ready state"

From that point, every warm instance in the pool is a resume of that snapshot — the OS doesn't reboot, the packages don't reinstall, the repo is already there. The first instruction the agent executes after a warm claim is your business logic, not setup.

Pool manager: keeping N instances warm

The pool manager runs a reconciliation loop at a configurable interval (default: 30 seconds). On each tick:

  1. Count live warm instances. Any instance in "ready" state that hasn't been claimed.
  2. Count recently claimed instances. Instances claimed in the last N seconds, to detect rapid burst.
  3. If warm count is below target: restore new instances from the snapshot, up to max_size.
  4. If warm instances are older than max_age_seconds: recycle them (terminate + replace) to prevent snapshot drift and apply security patches.
  5. Emit metrics: pool depth, utilization percentage, claim latency p50/p99, miss rate.
┌──────────────────────────────────────────────────────────────────────┐
│                         WARM POOL SYSTEM                             │
│                                                                      │
│  ┌──────────────────┐       restore      ┌──────────────────────┐   │
│  │  Snapshot Store  │ ─────────────────► │   Pool Manager       │   │
│  │                  │                   │                      │   │
│  │  base-image +    │                   │  [ready] [ready]     │   │
│  │  setup baked in  │ ◄─────────────── │  [ready] [ready]     │   │
│  └──────────────────┘   recycle old     │  [ready]             │   │
│                                         │                      │   │
│  ┌──────────────────┐  claim (200-800ms) │  reconcile every 30s │   │
│  │  Request Router  │ ─────────────────► └──────────────────────┘   │
│  │                  │                                               │
│  │  incoming req ──►│──► grab from pool ──► inject creds ──► run    │
│  │                  │                                               │
│  │  pool empty? ───►│──► cold start fallback (15-45s)              │
│  └──────────────────┘                                               │
│                                                                      │
│  ┌──────────────────┐   On task complete:                           │
│  │  Release Handler │   single_use → terminate + refill from snap   │
│  │                  │   reusable  → reset state + return to pool    │
│  └──────────────────┘                                               │
└──────────────────────────────────────────────────────────────────────┘

Request routing: claim → personalize → run

When a request arrives and claims a pool member, three things happen in sequence before your agent code runs:

  1. Claim (pool manager): an instance transitions from "ready" to "claimed." The pool manager immediately starts restoring a replacement from the snapshot. The net pool depth stays stable.
  2. Personalize (injected at claim time): user credentials, per-tenant environment variables, task parameters are injected via environment variables or a mounted secrets volume. The snapshot never contains these.
  3. Run: your agent code executes. The environment is fully pre-warmed; the agent starts working immediately.

What's pre-loaded at snapshot time vs injected per-request

Pre-loaded (snapshot time)Injected (per-request claim)
OS packages, system toolsUser API tokens (GitHub PAT, Slack token, etc.)
Claude Code CLI, Codex CLI, Aider, Goose, or other harnessPer-tenant database connection strings
Repository clone (your codebase)Task parameters (what file to review, which customer, etc.)
Node.js / Python / Go dependenciesPer-request correlation ID for tracing
Model weights (if running a local model)Dynamic config overrides
MCP server binariesTemporary credentials from Vault / AWS STS
Static analysis tools (linters, formatters)Per-user workspace path or branch name

The rule: anything that's the same for all requests → pre-load into the snapshot. Anything user-specific, tenant-specific, or time-sensitive → inject at claim time.

Reuse cycles: warm pool isn't always fresh

Two pool patterns:

Sandbox Platform supports both. Configure per pool.

The cost trade-off

StrategyIdle costP99 latencyWhen it wins
Cold-start on demand$015-30 secBursty / unpredictable load; latency tolerant
Warm pool of 5~$0.50/hr0.8 secPredictable load; customer-facing
Warm pool of 20$2.00/hr0.6 secHigh-traffic customer-facing
Always-on (no pool, dedicated)per-agent compute 24/70 secSingle high-priority agent that must never wait

Configuration

Warm pools are created via the Sandbox Platform API or dashboard. The full configuration object:

json — POST /api/pools (full configuration)
{
  "name": "claude-code-reviewer",
  "snapshot_id": "snap_abc123",        // which snapshot to restore from
  "min_size": 3,                         // floor — always keep at least 3 warm
  "target_size": 8,                      // steady-state pool depth
  "max_size": 20,                        // ceiling for auto-scale bursts
  "reuse_policy": "single_use",         // single_use | reusable
  "keepalive_seconds": 1800,            // idle warm instance TTL (30 min)
  "max_age_seconds": 86400,             // recycle members older than 24h
  "reconcile_interval_seconds": 30,     // how often pool manager checks depth
  "auto_refill": true,                  // refill immediately after each claim
  "on_empty": "cold_start_fallback",    // what to do when pool is empty
  "inject_env": {
    "HARNESS": "claude-code",           // static env injected at claim time
    "PLATFORM": "sandbox-platform"
  },
  "tags": {"team": "engineering", "tier": "premium"}
}

Key fields explained:

Claiming from the pool

The claim/release lifecycle is the integration point between your application and the pool. A production-grade implementation handles the claim, injects per-request credentials, wraps the task, releases (or acknowledges discard), and measures the latency to confirm you're actually getting warm starts.

python — production claim/release with credential injection and latency tracking
import requests, time, os, contextlib, logging
from contextlib import contextmanager

SANDBOX_API = os.environ["SANDBOX_API_URL"]
API_KEY = os.environ["SANDBOX_API_KEY"]
POOL_NAME = "claude-code-reviewer"

@contextmanager
def warm_sandbox(user_github_token: str, task_id: str):
    """
    Context manager: claim a warm sandbox, inject per-request creds,
    yield the sandbox info, then release on exit.
    """
    t0 = time.monotonic()

    # Claim a warm instance from the pool
    resp = requests.post(
        f"{SANDBOX_API}/api/pools/{POOL_NAME}/claim",
        json={
            "inject_env": {
                "GITHUB_TOKEN": user_github_token,  # per-request injection
                "TASK_ID": task_id,
                "ANTHROPIC_API_KEY": os.environ["ANTHROPIC_API_KEY"],
            }
        },
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=5,
    )
    resp.raise_for_status()
    data = resp.json()

    claim_latency_ms = (time.monotonic() - t0) * 1000
    logging.info(
        "pool_claim",
        extra={
            "pool": POOL_NAME,
            "sandbox_id": data["sandbox_id"],
            "was_warm": data.get("was_warm", True),
            "claim_ms": round(claim_latency_ms, 1),
            "task_id": task_id,
        }
    )

    # Alert if we got a cold-start fallback (pool was empty)
    if not data.get("was_warm", True):
        logging.warning("pool_miss: cold start fallback", extra={"pool": POOL_NAME})

    try:
        yield data   # caller gets sandbox_id, connection info, etc.
    finally:
        # Release back to pool (or discard if single_use — platform decides)
        requests.post(
            f"{SANDBOX_API}/api/pools/{POOL_NAME}/release",
            json={"sandbox_id": data["sandbox_id"]},
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=5,
        )

# Usage:
with warm_sandbox(user_github_token=user_token, task_id="review-pr-1234") as sb:
    # sb["sandbox_id"] — use to run commands in the sandbox
    # sb["connection"]["exec_url"] — WebSocket endpoint for exec
    # Sandbox is already running; Claude Code is already installed
    result = run_code_review(sb["sandbox_id"], pr_number=1234)

Key design decisions in this implementation:

Pre-warming strategies

How you pre-warm depends on your traffic pattern. Three strategies cover most production workloads.

Strategy 1: Static pre-warm

Keep a fixed pool of N warm instances at all times. min_size == target_size. The pool manager never scales down. Simple, predictable cost, zero pool-miss risk if sized correctly. Best for steady, predictable traffic.

json — static pre-warm: min = target = max
{
  "name": "support-agent-static",
  "snapshot_id": "snap_support_v4",
  "min_size": 5,
  "target_size": 5,
  "max_size": 5,          // no auto-scale — fixed fleet
  "reuse_policy": "reusable",
  "keepalive_seconds": 3600,
  "auto_refill": true
}

When it wins: your business-hours customer support queue handles 40-60 requests/hour with low variance. You know you need 5 agents. Static is cheaper than predictive (no scaling infrastructure) and simpler to reason about.

When it loses: if traffic drops to near zero overnight, you're paying for 5 idle agents. Scale to a smaller night pool or add a schedule-based scale-down.

Strategy 2: Predictive pre-warm

Scale the pool ahead of known traffic patterns. You know Monday morning at 8am brings a surge; pre-warm to N at 7:50am before users arrive. Use a cron-based schedule to drive pool resizing via the platform API.

python — cron job that pre-warms the pool 10 minutes before surge
import requests, os

POOL_NAME = "claude-code-reviewer"
API_KEY = os.environ["SANDBOX_API_KEY"]
BASE_URL = os.environ["SANDBOX_API_URL"]

# Runs at 07:50 Mon-Fri (UTC) via cron/EventBridge
def pre_warm_morning_surge():
    """Scale pool to 20 before the 8am engineering standup + PR surge."""
    requests.patch(
        f"{BASE_URL}/api/pools/{POOL_NAME}",
        json={"target_size": 20, "min_size": 15},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )

# Runs at 19:00 Mon-Fri (UTC) — scale back down after close of business
def scale_down_evening():
    requests.patch(
        f"{BASE_URL}/api/pools/{POOL_NAME}",
        json={"target_size": 3, "min_size": 2},
        headers={"Authorization": f"Bearer {API_KEY}"},
    )

Real example from a fintech team: their Devin-based code-review fleet processes 150 PRs/day. PR volume peaks 9am-noon and 2pm-5pm Pacific. They pre-warm 25 agents at 8:50am and 1:50pm, scale to 5 overnight. Monthly idle cost compared to static 25: 63% cheaper.

Strategy 3: On-demand burst

Keep a small minimum pool (2-3) for baseline traffic. When the queue starts building (more requests arriving than warm instances can serve), the pool manager auto-scales to max_size. The first batch of requests beyond the minimum pool size hits cold-start fallback; subsequent requests hit warm instances from the scale-out. Best for bursty, unpredictable traffic.

json — on-demand burst pool: small min, large max, auto-scale enabled
{
  "name": "batch-research-burst",
  "snapshot_id": "snap_research_v2",
  "min_size": 2,             // always keep 2 warm — cheap baseline
  "target_size": 2,          // sit at 2 when traffic is light
  "max_size": 30,            // scale to 30 on burst
  "scale_up_threshold": 0.8, // scale up when utilization > 80%
  "scale_up_step": 5,        // add 5 instances per scale-up tick
  "scale_down_delay_seconds": 300,  // wait 5 min after burst before scaling down
  "on_empty": "cold_start_fallback",
  "reuse_policy": "single_use"
}

The burst latency profile for this pattern: the first N requests beyond the warm minimum hit cold start (15-45s); as scale-out completes, subsequent requests get warm starts. If your burst workload is a fan-out (all 100 requests arrive simultaneously), the first 2 get warm starts, the remaining 98 start cold, and the pool refills from the snapshot in the background for the next burst.

Choosing between strategies:

StrategyBest forIdle costMiss risk
StaticSteady, predictable traffic; simplicityHigh (N instances always warm)Zero (if sized correctly)
PredictiveKnown daily/weekly traffic curves; cost optimizationMedium (scales with schedule)Low (if schedule is accurate)
On-demand burstBursty, unpredictable; cost-sensitiveLow (minimum baseline only)Medium (first-wave overflow hits cold start)

When warm pools are wrong

Cost model: the break-even analysis

Warm pool instances cost money even when idle. The tradeoff is simple: idle cost vs the value of the latency savings. For customer-facing agents, the math usually favors warm pools at lower request volumes than you'd expect.

Idle cost math

Example configuration: 2 warm instances on a ab0t.small compute tier.

Pool sizeCost per instance/hrHourly idle costDaily idle costMonthly idle cost
2 instances$0.008$0.016$0.38$11.70
5 instances$0.008$0.040$0.96$29.00
10 instances$0.008$0.080$1.92$58.00
20 instances$0.008$0.160$3.84$115.00

Break-even calculation

For a pool of 2 instances, the daily idle cost is $0.38. The question is: how many requests per day need to hit this pool before the latency savings justify that cost?

Assume:

Break-even at $0.38/day idle cost: 2 requests/day. Any agent serving more than 2 requests per day to an engineer (or a customer with measurable churn impact) breaks even on the pool cost.

For customer-facing agents, the calculation is even more favorable: latency-induced abandonment typically costs $5-50 per lost session depending on conversion value. A single prevented abandonment pays for a 2-instance pool for 13 days.

Cost vs throughput profile

Daily request volumePool recommendationMonthly pool costValue of 30s saved/request @ $0.50/minROI
< 10 requests/dayNo pool — cold start is fine$0< $2.50N/A
10-50 requests/dayPool of 2$11.70$2.50 - $12.50Marginal; justify by latency UX not pure cost
50-200 requests/dayPool of 3-5$17-29$12.50 - $50Positive ROI; pool clearly justified
200-1000 requests/dayPool of 5-15 + predictive scaling$29-87$50 - $250Strong ROI; invest in tuning the pool
> 1000 requests/dayPool of 15+ + burst scaling$87+$250+Pool cost is rounding error; optimize for reliability

The threshold where warm pools become obviously worth it for pure cost-ROI: ~50 agent interactions per day per pool. Below that, warm pools are a UX bet (users feel the difference, churn decreases) rather than a pure cost play.

Snapshot design: what to pre-load

The snapshot is the core of the warm pool. Design it wrong and you get either slow restores (too much pre-loaded) or slow first-use (not enough pre-loaded). Design it right and agents start working in milliseconds.

A production snapshot specification

This is a Dockerfile-style spec for a Claude Code code-review agent. The pattern applies to any harness (Codex CLI, Cursor, Aider, Goose) with substitutions.

dockerfile — snapshot base for a Claude Code reviewer pool
# Base: Ubuntu 22.04 LTS — standard, patched weekly
FROM ubuntu:22.04

# Layer 1: System tools (changes rarely → lowest layer)
RUN apt-get update && apt-get install -y \
    git curl wget jq ripgrep fd-find \
    python3.11 python3-pip nodejs npm \
    build-essential libssl-dev && \
    rm -rf /var/lib/apt/lists/*

# Layer 2: Claude Code CLI (pinned version — update triggers pool refresh)
RUN npm install -g @anthropic-ai/claude-code@1.2.3

# Layer 3: Python dependencies for the review harness
COPY requirements.txt /tmp/
RUN pip3 install -r /tmp/requirements.txt

# Layer 4: Repository clone (the codebase being reviewed)
# Use a deploy key — read-only, rotated quarterly
RUN mkdir -p /workspace && \
    git clone --depth=100 https://github.com/myorg/myrepo.git /workspace/repo

# Layer 5: MCP server for code tools (linters, formatters)
RUN npm install -g @modelcontextprotocol/server-code-tools

# IMPORTANT: Do NOT bake in any of these:
# - ANTHROPIC_API_KEY (inject at claim time)
# - GitHub PAT (inject at claim time)
# - Database URLs (inject at claim time)
# - User-specific config (inject at claim time)

# Snapshot point: everything above this is frozen in the pool
# Everything below is injected per-request

What NOT to pre-load (security and correctness)

Pre-loading the wrong things into a snapshot creates security vulnerabilities that are hard to notice and easy to exploit.

Do NOT pre-loadWhyHow to handle instead
User API tokens (GitHub PAT, Anthropic API key, Slack token)The snapshot is shared across all pool members and all requests. A pre-loaded token is available to every request the pool ever serves.Inject via environment variables at claim time. Rotate tokens independently of the pool.
Database connection strings with credentialsLive connections timeout and become invalid between snapshot creation and instance restore. Connection strings with passwords are a snapshot-persistence risk.Inject the connection string at claim time. Use IAM-based auth (e.g. AWS RDS IAM) where possible — no password in the connection string.
Per-user or per-tenant workspace statePool members are shared; a previous user's working directory would be visible to the next user in a reusable pool.Each claim gets a fresh per-request workspace mounted at a unique path. The snapshot has an empty workspace directory.
Temporary AWS/GCP credentialsShort-lived credentials expire while the snapshot is at rest. The warm instance will fail its first operation trying to use a 1-hour STS token that's 6 hours old.Fetch fresh credentials via the metadata service or a secrets manager at instance startup, not at snapshot time.
Model weights for non-deterministic modelsNot a security issue, but weights that change between snapshot builds cause divergent behavior across the pool. Some members use old weights, some new.Pin model version in the snapshot. When you update weights, invalidate the pool and rebuild from the new snapshot.

Snapshot TTL and rotation

A snapshot becomes stale when:

Recommended: rebuild the snapshot weekly via CI. The rebuild CI job pushes the new snapshot ID to the pool config (PATCH /api/pools/{name} with the new snapshot_id). The pool drains old members and refills from the new snapshot over the next reconcile cycle. Zero downtime, always fresh.

Monitoring warm pools in production

A warm pool that isn't monitored is a pool you can't tune. Four metrics drive all pool decisions; three alerts cover the failure modes you'll actually hit.

The four pool metrics

MetricDescriptionTargetAction if out of range
pool_depthCurrent number of warm (unclaimed) instancesBetween min_size and target_size during peak hoursIf at floor continuously: increase min_size or target_size
pool_utilization_pctClaimed instances / (claimed + unclaimed)50-80% during peak hoursAbove 80%: scale up. Below 30%: scale down.
claim_latency_ms (p99)Time from claim request to sandbox readyUnder 800ms for warm hitsIf above 1s: pool may be restoring from snapshot (temporary); if sustained, increase pool size
pool_miss_ratePercentage of claims that got a cold-start fallback0% during normal operationsAny non-zero: pool is undersized for current traffic. Scale up target_size immediately.

Three alerts to configure on day one

yaml — alert definitions (works with Prometheus Alertmanager, Datadog, or CloudWatch)
# Alert 1: pool is running on fumes
- name: PoolDepthCritical
  condition: pool_depth < min_size AND time_of_day IN business_hours
  severity: warning
  message: "Pool {pool_name} is below minimum depth. Claims will cold-start on next burst."
  action: "PATCH /api/pools/{pool_name} to increase target_size, or investigate why pool isn't refilling"

# Alert 2: pool misses are happening
- name: PoolMissDetected
  condition: pool_miss_rate > 0 FOR 5_minutes
  severity: warning
  message: "Pool {pool_name} miss rate is {rate}%. Users are hitting cold starts."
  action: "Increase min_size immediately; review traffic pattern for predictive scaling"

# Alert 3: claim latency is degraded (pool restore is slow)
- name: ClaimLatencyDegraded
  condition: claim_latency_p99 > 2000ms FOR 10_minutes
  severity: info
  message: "Pool {pool_name} p99 claim latency is {latency}ms. Pool may be restoring from snapshot."
  action: "Check pool_depth and reconcile cycle logs; may resolve automatically"

What a healthy pool dashboard looks like

For a pool of 8 serving a customer-facing Claude Code assistant with 100 daily active users:

Any sustained claim latency above 2 seconds, or any miss rate above 5% for more than 5 minutes, signals the pool is under-provisioned for current traffic.

vs Browserbase, E2B, Daytona

Each platform handles cold starts differently:

Common mistakes from the field

The teams that have struggled with warm pools in production consistently make the same six mistakes. All of them are avoidable with the right configuration upfront.

Mistake 1: Pre-loading user credentials into the snapshot

This is the most dangerous mistake and the hardest to detect until it causes an incident. A team building a Claude Code-based code assistant baked their GitHub Personal Access Token into the Docker image for convenience. Every warm pool instance — shared across all customers — had that token. A customer making a malicious request could read any repository the token had access to.

Fix: Never include tokens, passwords, or secrets in the snapshot. Inject via environment variables at claim time. Audit your Dockerfile and setup scripts for any ENV GITHUB_TOKEN= or similar lines before registering a snapshot.

Mistake 2: Pool size too small for burst traffic

A team sized their pool for average traffic (8 requests/hour) with a pool of 2. Their Monday morning surge hit 40 requests in 5 minutes. The first 2 got warm starts; the remaining 38 hit cold-start fallback. Users who signed up on the weekend got a 30-second wait on their first interaction and churned at a higher rate than baseline.

Fix: Size for p99 concurrent requests, not average. For bursty traffic, use on-demand burst configuration with a generous max_size. Watch pool-miss-rate in your metrics; a non-zero miss rate during business hours means you're undersized.

Mistake 3: Pool size too large for actual traffic

The inverse mistake: a team scared of pool misses provisioned 20 warm Claude Code agents for a product with 15 daily active users. Utilization was consistently 3-5%. They were paying $115/month in idle compute for a product that needed 3 instances at peak.

Fix: Start with pool size = 3. Watch pool-utilization for two weeks. Scale up if utilization is consistently above 70%. Scale down if it's below 30% during peak hours. Use predictive scaling to match your actual traffic curve.

Mistake 4: Not configuring snapshot TTL

A team set up their first warm pool with no max_age_seconds. Pool members accumulated — some were 90 days old by the time someone noticed. The old members were running an unpatched OS, a deprecated Claude Code CLI version, and a 90-day-old repo clone. New code had shipped; old code reviews were using the stale clone.

Fix: Always set max_age_seconds. For security-sensitive workloads: 24-48 hours. For general-purpose agent pools: 7 days. Also set keepalive_seconds for idle instances that aren't being used — they should recycle and be replaced with fresh snapshots.

Mistake 5: Forgetting to invalidate the pool when dependencies update

A team updated their Anthropic SDK from 0.25 to 0.30 (breaking change in the tool-call API). The new version was in the Dockerfile; they built a new image. But they forgot to register a new snapshot and update the pool's snapshot_id. The pool continued restoring from the old snapshot. Code reviews failed with cryptic tool-call errors for two hours before someone correlated the SDK update with the failures.

Fix: Make snapshot registration and pool update part of your CI pipeline. When the Dockerfile changes, CI automatically builds a new snapshot, registers it, and patches the pool config with the new snapshot_id. Never manually manage snapshot versions in a pool that sees real traffic.

Mistake 6: Treating warm pools as a substitute for efficient agent code

A team hit their latency SLA by switching to warm pools. Their interactive Claude Code assistant went from 30-second cold-start to 800ms warm-start. Success. Except the agent's first substantive operation was a 5-minute full-codebase scan that ran on every request, regardless of what the user asked. The warm start bought them 29 seconds; the unnecessary scan cost them 5 minutes.

Fix: Profile your agent's first 60 seconds of work after a warm claim. Warm pools eliminate environment setup latency; they don't fix application-level inefficiencies. An agent that does unnecessary work on startup benefits less from warm pools than one that starts immediately on the actual task.

Frequently asked questions

What is a warm pool in AI agent infrastructure?

A warm pool is a set of pre-provisioned sandboxes that are already running (or suspended at a known checkpoint) and ready to claim instantly. Instead of booting a new VM and installing dependencies on every request (15-45 seconds), the platform assigns a pre-warmed instance in 200-800ms. The agent starts working immediately; the environment setup already happened before the request arrived.

How does snapshot-based warm pool startup work?

The platform freezes a running container to disk at a known-good state (the snapshot). Warm pool instances are resumed from that snapshot — the OS, filesystem, and process memory are restored in one operation (200-800ms). This is fundamentally faster than cold start because the OS boot, dependency install, and app initialization already happened when the snapshot was created.

What should I pre-load into the snapshot?

Pre-load everything that doesn't change per-request: OS packages, the Claude Code CLI, Codex CLI, or other agent harness, the repository clone, dev dependencies (node_modules, pip packages, etc.), and model weights if you're running a local model. Do NOT pre-load user tokens, session credentials, live database connections, or any per-user state. Those are injected at request time after the warm instance is claimed.

What is the real latency difference between cold start and warm pool?

Cold start: 15-45 seconds (EC2 boot + EBS attach + cloud-init + Docker pull + app startup). Warm pool claim: 200-800ms (snapshot restore + per-request injection). The 22× improvement is the core business case for warm pools in customer-facing workloads. For interactive coding assistants like Claude Code, the difference between 30 seconds and 400ms is the difference between a user waiting and a user not noticing.

How do I size a warm pool?

Formula: pool_size = ceil(p99_concurrent_requests × buffer_factor). Buffer factor is 1.2-1.5 depending on traffic peakiness. Example: customer-facing agent serving 50 customers, p99 concurrent is 8 — pool size is ceil(8 × 1.3) = 11. Run at this for a week, then tune based on pool-miss-rate and pool-utilization metrics.

What does a warm pool cost when idle?

Each warm instance consumes compute even when not serving requests. Example: 2 warm instances on a ab0t.small at $0.008/hr each = $0.016/hr = $0.38/day idle cost. For 5 instances: $0.95/day. The break-even question is how many requests per day does your pool handle before the latency savings justify the idle cost? For customer-facing agents, even 10 requests/day usually justifies a pool of 2.

What is the difference between single-use and reusable pool members?

Single-use: claim, use, terminate. The pool refills with a fresh instance from the snapshot. Best for security and isolation — the next user never sees anything from the previous one. Reusable: claim, use, reset state, return to pool. Faster (no refill cost) but requires thorough state reset. Best for stateless agents like browsers with cleared cookies or code execution sandboxes with reset workspaces.

What are the three pre-warming strategies?

Static pre-warm keeps a fixed pool size N at all times — simple, predictable cost, best for steady traffic. Predictive pre-warm scales the pool based on traffic patterns (pre-warm 10 instances at 7:50am before the 8am surge). On-demand burst keeps a small minimum pool (2-3) and scales to a larger pool (20+) automatically when queue depth grows. Each has different cost and latency profiles; see the strategies section for configuration examples.

When should I not use warm pools?

Skip warm pools when: each agent carries per-tenant identity that can't be shared across pool members; traffic is bursty and unpredictable (small pool wastes money idle and overflows on burst); it's an overnight batch job where 30s cold start doesn't matter; or state must persist — pools recycle members, so persistent state needs a dedicated sandbox. See Persistent Workspaces.

How do I prevent credentials from leaking between pool requests?

Never pre-load credentials into the snapshot. Credentials are injected at claim time via environment variables or a secrets manager call. With single-use pools, the instance is terminated after each use so there's no cross-request contamination. With reusable pools, your reset step must explicitly clear any injected credentials before the instance returns to the pool.

How does predictive pre-warming work?

Predictive pre-warming uses historical traffic data to scale the pool before a known surge. Configure a cron-based scaling schedule: scale to N at 7:50am Monday-Friday (before the 8am work surge), scale back to the minimum at 7pm. The platform's scheduler handles the scale-up; warm instances are ready before the surge arrives. See the predictive strategy section for a code example.

What happens when the pool is empty and a request arrives?

The request falls back to a cold-start — the platform provisions a new sandbox on-demand (15-45 seconds). This is called a pool miss. The pool manager immediately starts refilling the pool in the background. Pool misses are logged; alert on miss rate to detect under-provisioning.

Should I use warm pools or custom Docker images to reduce cold-start time?

Both. Custom Docker images (pre-baked dependencies, no pull-time) reduce cold-start from 30s to 8-12s. Warm pools reduce it to 200-800ms. They're complementary: a warm pool of custom-image-based sandboxes gives the fastest possible start and the fastest pool refill when a pool miss occurs. Start with custom images; add warm pools when sub-second latency becomes critical.

How do I invalidate the pool when my snapshot is stale?

Configure max_age_seconds on the pool (e.g. 86400 = 24 hours). Pool members older than this are recycled and replaced with fresh instances from the current snapshot. When you push a new snapshot version, patch the pool's snapshot_id and the pool drains old members and refills from the new snapshot within one reconcile cycle. Automate this in CI so it happens on every Dockerfile change.

How does Sandbox Platform warm pools compare to Browserbase?

Browserbase pioneered warm pools for browser containers and it's their core product differentiator. Sandbox Platform supports warm pools for any sandbox type — browsers, terminals, desktop environments, code execution sandboxes — so you can pool Claude Code environments, Codex CLI runtimes, or any custom container alongside browsers. Browserbase wins if you only need browsers; Sandbox Platform wins when you need multi-type pools or want to pool your own custom agent harness.

What's next

Set up your first warm pool

5 sandboxes, sub-second claim, ~$0.50/hr. Configurable in the dashboard.

Open Dashboard