Vercel Logo

Use Snapshots to Skip the Cold Start

The benchmark numbers from 5.1 told us where the time goes. Most of it lives inside pnpm install, which makes sense because cold Sandboxes start with no node_modules and have to fetch everything from scratch.

Snapshots are how we reuse prepared filesystem state. A snapshot captures a running Sandbox's files and installed packages; it is distinct from the container image used to boot the Sandbox. Restoring one can skip toolchain setup or reuse a warmed package cache.

A snapshot containing one repository's node_modules will not generally accelerate an unrelated repository. Aim for reusable toolchains and package caches, or create a snapshot specifically for a stable repository.

Outcome

Update runSandboxLifecycle to create the Sandbox from a configured snapshot ID. If no ID is configured, create a default throwaway Sandbox. If a configured ID is invalid, surface the error instead of disguising every failure as a missing snapshot.

Fast Track

  1. Read a snapshot ID from SANDBOX_SNAPSHOT_ID.
  2. Restore with source: { type: 'snapshot', snapshotId }.
  3. Use default creation only when the env var is absent.

Hands-on exercise

Open src/sandbox-lifecycle.ts. We're going to extract Sandbox creation into a small helper:

import { Sandbox } from '@vercel/sandbox';
import { detectPackageManager } from './test-runner';
 
const INTERESTING_PATHS = [
  'repo/package.json',
  'repo/src/index.ts',
  'repo/src/app.ts',
  'repo/lib/auth.ts'
];
 
const SNAPSHOT_ID = process.env.SANDBOX_SNAPSHOT_ID;
 
async function createSandbox(): Promise<{ sandbox: Sandbox; usedSnapshot: boolean }> {
  if (!SNAPSHOT_ID) {
    console.warn('SANDBOX_SNAPSHOT_ID is not set; using a default Sandbox.');
    const sandbox = await Sandbox.create({ persistent: false, timeout: 10 * 60 * 1000 });
    return { sandbox, usedSnapshot: false };
  }
 
  const sandbox = await Sandbox.create({
    source: { type: 'snapshot', snapshotId: SNAPSHOT_ID },
    persistent: false,
    timeout: 10 * 60 * 1000
  });
  return { sandbox, usedSnapshot: true };
}
 
export type TestResult = {
  exitCode: number;
  stdout: string;
  stderr: string;
  packageManager: string;
};
 
export type LifecycleResult = {
  sandboxName: string;
  usedSnapshot: boolean;
  cloneExitCode: number;
  files: Array<{ path: string; content: string }>;
  testResult: TestResult;
};
 
export async function runSandboxLifecycle(repoUrl: string): Promise<LifecycleResult> {
  const { sandbox, usedSnapshot } = await createSandbox();
 
  try {
    const clone = await sandbox.runCommand('git', ['clone', '--depth', '1', repoUrl, 'repo']);
    if (clone.exitCode !== 0) {
      throw new Error(`Clone failed: ${await clone.stderr()}`);
    }
 
    const files: Array<{ path: string; content: string }> = [];
    for (const fullPath of INTERESTING_PATHS) {
      const content = await sandbox.readFileToBuffer({ path: fullPath });
      if (content) {
        files.push({
          path: fullPath.replace(/^repo\//, ''),
          content: content.toString('utf8')
        });
      }
    }
 
    const pm = await detectPackageManager(sandbox);
    const install = await sandbox.runCommand(pm.install);
    if (install.exitCode !== 0) {
      throw new Error(`Install failed: ${await install.stderr()}`);
    }
    const test = await sandbox.runCommand(pm.test);
 
    return {
      sandboxName: sandbox.name,
      usedSnapshot,
      cloneExitCode: clone.exitCode,
      files,
      testResult: {
        exitCode: test.exitCode,
        stdout: await test.stdout(),
        stderr: await test.stderr(),
        packageManager: pm.name
      }
    };
  } finally {
    await sandbox.stop();
  }
}

Three things to flag.

The snapshot ID is configurable via SANDBOX_SNAPSHOT_ID. Snapshot IDs, rather than user-defined names, are what Sandbox.create({ source }) restores.

The fallback is only for an absent configuration. A configured but expired, deleted, or inaccessible snapshot should fail loudly so authentication and service errors are not misreported.

The usedSnapshot flag flows back through the result so the CLI (or the reporter we build in 5.4) can show "snapshot accelerated" in the summary. Knowing whether the snapshot path was actually taken is the difference between "we have snapshots" and "snapshots actually help."

To create one, prepare a Sandbox and call snapshot(). Snapshotting stops that Sandbox automatically:

const base = await Sandbox.create({ persistent: false });
await base.runCommand('corepack', ['enable']);
const snapshot = await base.snapshot({ expiration: 14 * 24 * 60 * 60 * 1000 });
console.log(snapshot.snapshotId);

Snapshots expire after 30 days by default. Pass an explicit expiration when your retention needs differ.

Troubleshooting: snapshot restore fails

Confirm SANDBOX_SNAPSHOT_ID contains a current snapshot ID and that your Vercel credentials can access its project. Remove the variable only when you intentionally want default creation; don't catch every restore error as a fallback.

Troubleshooting: snapshot creation isn't free either

Creating a snapshot includes the setup run and storage. Reuse it enough to justify that work, and measure the actual benefit for your repository mix.

Try It

Run without SANDBOX_SNAPSHOT_ID:

pnpm review https://github.com/vercel/examples

Expected output:

SANDBOX_SNAPSHOT_ID is not set; using a default Sandbox.
Reviewing https://github.com/vercel/examples...
  ⏱  sandbox lifecycle: 24180ms
  ⏱  ai analysis: 7240ms

Total: 31420ms

After exporting a valid snapshot ID and running again:

Reviewing https://github.com/vercel/examples...
  ⏱  sandbox lifecycle: 9420ms
  ⏱  ai analysis: 7180ms

Total: 16600ms

The unconfigured warning is gone. Your measured improvement depends on what the snapshot contains and how closely the reviewed repo matches it. AI analysis time should remain roughly the same because snapshots do not accelerate model calls.

Commit

git add src/sandbox-lifecycle.ts
git commit -m "feat(sandbox): create from named snapshot with graceful fallback"

Done-When

  • Sandbox.create({ source: { type: 'snapshot', snapshotId } }) restores a configured snapshot
  • Default creation runs only when SANDBOX_SNAPSHOT_ID is absent
  • Invalid configured snapshot errors remain visible
  • usedSnapshot flag is returned in LifecycleResult
  • SANDBOX_SNAPSHOT_ID selects the snapshot to restore

Solution

src/sandbox-lifecycle.ts (helper)
const SNAPSHOT_ID = process.env.SANDBOX_SNAPSHOT_ID;
 
async function createSandbox(): Promise<{ sandbox: Sandbox; usedSnapshot: boolean }> {
  if (!SNAPSHOT_ID) {
    console.warn('SANDBOX_SNAPSHOT_ID is not set; using a default Sandbox.');
    const sandbox = await Sandbox.create({ persistent: false, timeout: 10 * 60 * 1000 });
    return { sandbox, usedSnapshot: false };
  }
 
  const sandbox = await Sandbox.create({
    source: { type: 'snapshot', snapshotId: SNAPSHOT_ID },
    persistent: false,
    timeout: 10 * 60 * 1000
  });
  return { sandbox, usedSnapshot: true };
}

Was this helpful?

supported.