Skip to content
Sandbox SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Clean up sandboxes

Stop paying for sandboxes nobody uses. Pick the right limits for your provider, schedule a reaper, scope it with labels, and check what is still running.

Cloud sandboxes cost money while they run, and a keyed sandbox is meant to outlive the request that made it. If your app forgets one, it keeps running. This guide sets up two layers of protection: limits the provider enforces on its own, and a reaper you run on a schedule.

For what close, destroy, and await using do, see Lifecycle and cleanup.

1. Label every sandbox

Labels let you find your sandboxes later without touching anyone else’s. The SDK adds sandbox-sdk to every sandbox it creates, and you add your own:

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";

await using box = await createSandbox(e2b(), {
  key: "user:123",
  labels: { app: "builder", env: "production" },
});

Use one label per app and environment, so a staging cron job can never reap production sandboxes.

2. Set the limits your provider enforces

A limit the provider enforces holds even if your server crashes. Set the ones your provider supports on every keyed sandbox:

E2B

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";

await using box = await createSandbox(e2b(), {
  key: "user:123",
  timeoutMs: 60 * 60_000,
});

E2B kills the sandbox when timeoutMs runs out, and reconnecting never pushes the deadline back. It has no idle timer, so idleTimeoutMs fails with unsupported.

Daytona

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { daytona } from "@opencoredev/sandbox-sdk/daytona";

await using box = await createSandbox(daytona(), {
  key: "user:123",
  idleTimeoutMs: 15 * 60_000,
  timeoutMs: 8 * 60 * 60_000,
});

Daytona stops an idle sandbox and keeps its disk, and deletes it at the lifetime limit. Both work in whole minutes: idle rounds up, lifetime rounds down.

Vercel Sandbox

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { vercel } from "@opencoredev/sandbox-sdk/vercel";

await using box = await createSandbox(vercel(), {
  key: "user:123",
  labels: { app: "builder" },
  timeoutMs: 45 * 60_000,
});

Vercel Sandbox has no idle timer, so idleTimeoutMs fails with unsupported. timeoutMs limits one session, and each resume starts a new one, so a sandbox your users keep resuming never reaches a final deadline. Run the reaper below as well.

Local

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";

await using box = await createSandbox(local(), {
  key: "user:123",
  idleTimeoutMs: 30 * 60_000,
  timeoutMs: 4 * 60 * 60_000,
});

Docker has no timer. The SDK stores both deadlines on the container and enforces them when a client reconnects or the reaper runs.

Memory

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { memory } from "@opencoredev/sandbox-sdk/memory";

await using box = await createSandbox(memory(), { idleTimeoutMs: 1_000 });

Timers run in your process. Everything is gone when the process exits anyway.

3. Schedule a reaper

reapSandboxes destroys every sandbox that matches and returns how many it removed. Run it from a cron job, a scheduled function, or a queue worker:

import { reapSandboxes } from "@opencoredev/sandbox-sdk";
import { vercel } from "@opencoredev/sandbox-sdk/vercel";

const removed = await reapSandboxes(vercel(), {
  labels: { app: "builder", env: "production" },
  olderThanMs: 24 * 60 * 60_000,
  idleOnly: true,
});

console.log(`reaped ${removed} sandboxes`);
  • labels limits reaping to your app. Always pass it.
  • olderThanMs skips sandboxes younger than this, measured from creation.
  • idleOnly: true skips any sandbox with an SDK command in flight or an SDK background process running. Commands started outside the SDK aren’t seen. On E2B and Upstash, a process an SDK command left running, such as npm run dev &, also keeps the sandbox busy until it exits.

Run it about as often as your shortest acceptable waste. Hourly is a good start. On Next.js, a cron route works.

4. See what is running

listSandboxes returns every sandbox this SDK created, filtered by labels. It never lists sandboxes other tools made in the same account.

import { listSandboxes } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";

const boxes = await listSandboxes(e2b(), { labels: { app: "builder" } });
for (const b of boxes) console.log(b.id, b.key, b.state, new Date(b.createdAt));

state is "running", "paused", or "stopped". A count that keeps growing means a code path creates keyed sandboxes it never cleans up.

5. Destroy what you’re done with

When a user deletes a project or an account, destroy its sandbox instead of waiting for the reaper:

import { connectSandbox, listSandboxes } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";

const provider = e2b();
const [info] = (await listSandboxes(provider, { labels: { app: "builder" } })).filter(
  (b) => b.key === "project:42",
);

if (info) {
  const box = await connectSandbox(provider, info.id);
  await box.destroy();
}

Looking the sandbox up first means a project that never had one doesn’t get one created just to be deleted.

A sandbox without a key is destroyed automatically at the end of its await using block.

Checklist

  • Every sandbox has an app label.
  • Every keyed sandbox has the limits its provider supports.
  • A reaper runs on a schedule for each provider you use, scoped by label.
  • Deleting a user or project destroys its sandbox.

Was this page helpful?