---
title: Sandboxes and keys
description: Create a sandbox, give it a key so you get the same one back next time, and reattach to it from any server process.
---

A sandbox is one isolated Linux machine at a provider. You get a handle to it from `createSandbox`, and every other call goes through that handle. Most apps want one sandbox per user or per project that survives between requests. A `key` gives you that.

## Create a sandbox

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

await using box = await createSandbox(e2b(), {
  env: { NODE_ENV: "development" },
  labels: { app: "builder" },
});

console.log(box.id, box.provider);
```

`box.id` is the provider's native ID. `box.provider` is the provider name, here `"e2b"`. The full option list is on the [reference page](/docs/reference/sandbox#createsandboxprovider-options).

## Get or create with a key

Pass `key` and `createSandbox` looks for a sandbox with that key first. It creates one only if none exists.

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

const userId = "user_123";

await using box = await createSandbox(e2b(), { key: `user:${userId}` });

if (box.created) {
  await box.git.clone("https://github.com/vercel/next.js", { dir: "app", depth: 1 });
}
```

`box.created` is `true` when this call made the sandbox and `false` when it found an existing one. Use it to run setup only once.

With a key, `await using` calls `box.close()` at the end of the block, not `destroy()`. The sandbox keeps running so the next request finds it. See [Lifecycle and cleanup](/docs/concepts/lifecycle-and-cleanup).

## Exact and best-effort keys

How reliable the lookup is depends on the provider.

| Provider                              | Key mode          | How the key is stored                                                            |
| ------------------------------------- | ----------------- | -------------------------------------------------------------------------------- |
| [Local](/docs/providers/local)             | exact             | Container name, a hash of the key                                                |
| [Vercel](/docs/providers/vercel)           | exact             | Sandbox name, a hash of the key                                                  |
| [Memory](/docs/providers/memory)           | exact             | In-process map                                                                   |
| [Local (agentOS)](/docs/providers/agentos) | exact, in-process | In-process map                                                                   |
| [E2B](/docs/providers/e2b)                 | best-effort       | Sandbox metadata                                                                 |
| [Daytona](/docs/providers/daytona)         | best-effort       | Sandbox labels                                                                   |
| Every other cloud provider            | best-effort       | Metadata, the VM name, or a marker file in the VM. Each provider page says which |

Exact means the provider enforces unique names, so two concurrent calls with the same key always land on one sandbox. Best-effort means the SDK finds the sandbox by searching metadata. Two requests that arrive at the same moment can both miss and create two sandboxes.

If that race matters, pass a `locator`. It is two functions backed by any key-value store you already run.

```ts
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
import { kv } from "./kv";

await using box = await createSandbox(e2b(), {
  key: "project:42",
  locator: {
    get: (key) => kv.get(key),
    set: (key, id) => kv.set(key, id),
    delete: (key) => kv.delete(key),
  },
});
```

The SDK reads the ID from `get`, and writes a new one with `set` after it creates a sandbox. If the stored ID points at a sandbox that no longer exists, the SDK creates a new one and stores that ID instead. Within one process, the SDK also queues calls for the same key, so they never race each other.

Across servers, `get` followed by `set` is not atomic, so these two functions alone can't stop two servers from creating a sandbox each. For a guarantee, add `withLock` and ask for `keyMode: "exact"`:

```ts
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
import { kv } from "./kv";

await using box = await createSandbox(e2b(), {
  key: "project:42",
  keyMode: "exact",
  locator: {
    get: (key) => kv.get(key),
    set: (key, id) => kv.set(key, id),
    withLock: (key, action) => kv.withLock(`sandbox:${key}`, action),
  },
});
```

`withLock` must run `action` while holding a distributed lock or transaction for that key. `keyMode: "exact"` on a best-effort provider without `withLock` fails with `unsupported`.

## Reattach by ID

`connectSandbox` attaches to a sandbox you already know the ID of. It never creates one.

```ts
import { connectSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";

const box = await connectSandbox(e2b(), "sbx_abc123");
const result = await box.run("ls");
console.log(result.stdout);
await box.close();
```

It throws `SandboxError` with code `not_found` if the sandbox is gone.

## What a handle exposes

| Member             | What it is                                     |
| ------------------ | ---------------------------------------------- |
| `box.id`           | Native sandbox ID                              |
| `box.key`          | The key you passed, if any                     |
| `box.provider`     | Provider name, such as `"vercel"`              |
| `box.created`      | Whether this call created the sandbox          |
| `box.labels`       | Labels on the sandbox, including `sandbox-sdk` |
| `box.capabilities` | Optional features this provider supports       |
| `box.native`       | The provider's own API, typed per provider     |

Use `box.native` when you need a provider feature the SDK doesn't wrap. For providers that call their API directly, it is a [`NativeApi`](/docs/reference/native) that calls any endpoint and can load the official SDK on demand. For providers that wrap an SDK, it is the same object that SDK returns.

## Optional capabilities

Some methods exist only on providers that support them: `pause()`, `resume()`, `snapshot()`, and `terminal()`. They are on the TypeScript type only when the provider has them, so calling `box.pause()` on a provider without pause is a type error, not a runtime surprise. `box.capabilities` holds the same set at runtime.

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

await using box = await createSandbox(e2b(), { key: "project:42" });

await box.run("npm install");
await box.pause();
```

The [capability matrix](/docs/reference/capabilities) lists which provider supports what.
