---
title: "Give every user of your product their own sandbox"
description: "Run each user's code in a sandbox of their own: a key for your backend, a project with a monthly cap, a sandbox found again by the user's id, paused when idle and capped in dollars, and the self-host question answered."
url: "https://www.promigence.ai/docs/guides/sandbox-per-user"
site: "Promigence"
---

# Give every user of your product their own sandbox

Run each user's code in a sandbox of their own: a key for your backend, a project with a monthly cap, a sandbox found again by the user's id, paused when idle and capped in dollars, and the self-host question answered.

An agent product that runs code for its users needs a computer per user. A user's files should still be there on
their next message, their code should never touch anyone else's, and nobody should be able to run up your bill. This
guide sets that up on Promigence and answers the question that comes first: build it yourself, or use a managed
service.

## Should I self-host or use a managed sandbox service for my agent product?

Use a managed sandbox service unless you have a hard reason to run your own, because isolating code nobody reviewed,
starting sandboxes fast, pausing idle ones and capping spend add up to a product of their own. On Promigence each user
gets a sandbox labelled with their id, found again on their next message, paused when idle and capped in dollars,
all behind a key that can start work but cannot touch keys, secrets or billing. Self-host when your users' data must stay inside your own
cloud account and a conversation with Promigence about running it there does not settle it.

## Self-host or managed

Self-hosting gives you full control and a long list of jobs. You choose an isolation boundary strong enough for code
nobody reviewed and keep it patched, build the image pipeline, start sandboxes fast enough that a user is not left
waiting, pause idle ones so they stop costing money, cap what one user can spend, write egress rules and keep secrets
out of reach. Then someone on your team carries the pager for all of it.

It is the right call in three cases: the data cannot leave your own cloud account, you already run a platform team
that operates isolation for untrusted code, or your sandboxes are busy around the clock at a scale where owned
machines cost less than per-second billing. Everyone else ships sooner on a managed service and keeps the option to
move later. The comparison is in [At a glance](#at-a-glance).

## 1. A project, a cap and a key for your backend

Keep production apart from staging with one project each, cap what production can spend in a month, and give your
backend a key that can run sandboxes and cannot touch keys, secrets or billing:

```bash
promigence project create myapp-prod
promigence project cap myapp-prod 1000
promigence keys create --project myapp-prod --name backend --access run --expires 90d
```

Store the key your backend gets as `PROMIGENCE_API_KEY`. A project that reaches its monthly cap refuses new work, and
each sandbox's own spend cap bounds what is already running.

## 2. The environment users start from

Every user's sandbox starts as a fresh copy of one snapshot. Put in it what your users' code needs, and a verify
command that proves it imports:

```bash
promigence snapshot create --image python:3.12 \
  --setup "pip install pandas matplotlib" \
  --verify "python -c 'import pandas, matplotlib'" \
  --alias user-env --wait --wait-timeout 10m
```

Nothing is stored if the verify command fails. The build is billed per second while it runs, whether it verifies
or not, up to $0.17 at the default small size ([quickstart](https://www.promigence.ai/docs/quickstart)), and every
user's sandbox then starts from this snapshot with no further build charge.

## 3. One sandbox per user, found again on the next message

Label each sandbox with the user's id when you create it. When the user writes again, list by that label and reuse
what you find; a paused sandbox comes back as it was. The idle rule pauses a sandbox after five minutes with no
request, and paused time is not billed. In TypeScript, run as an ES module:

```typescript
import { PromigenceClient, Sandbox } from "@promigence/sdk";

const client = new PromigenceClient(); // PROMIGENCE_API_KEY: the backend key from step 1
const opening = new Map<string, Promise<Sandbox>>();

async function openSandbox(userId: string): Promise<Sandbox> {
  const [found] = await Sandbox.list({ client, labels: { user: userId }, state: ["ready", "running", "paused"], limit: 1 });
  if (found) {
    const sbx = await Sandbox.connect(found.sandbox_id, { client });
    if (found.state === "paused") await sbx.resume();
    return sbx;
  }
  const [sbx] = await Sandbox.fork("user-env", {
    client,
    count: 1,
    spendCap: 2,
    timeoutMs: 60 * 60_000,
    metadata: { user: userId },
    allowHosts: ["pypi.org", "files.pythonhosted.org"],
  });
  if (!sbx) throw new Error("no sandbox was started");
  await sbx.setAutoPause(300);
  return sbx;
}

// One lookup per user at a time, so two quick messages never start two sandboxes.
export function sandboxFor(userId: string): Promise<Sandbox> {
  let p = opening.get(userId);
  if (!p) {
    p = openSandbox(userId).finally(() => opening.delete(userId));
    opening.set(userId, p);
  }
  return p;
}

// The tool your agent calls: run the model's code in this user's sandbox, return what it printed.
export async function runCode(userId: string, code: string): Promise<string> {
  const sbx = await sandboxFor(userId);
  await sbx.files.write("/tmp/main.py", code);
  const r = await sbx.commands.run("python3 /tmp/main.py", { timeoutMs: 60_000, throwOnExit: false });
  return r.exitCode === 0 ? r.stdout : r.stdout + r.stderr;
}
```

The same in Python, for a threaded backend:

```python
import threading
from typing import Dict

from promigence import Promigence, Sandbox

client = Promigence()  # PROMIGENCE_API_KEY: the backend key from step 1
locks: Dict[str, threading.Lock] = {}
locks_guard = threading.Lock()

def sandbox_for(user_id: str) -> Sandbox:
    with locks_guard:
        lock = locks.setdefault(user_id, threading.Lock())
    with lock:  # one lookup per user at a time
        found = Sandbox.list(client=client, labels={"user": user_id}, state=["ready", "running", "paused"], limit=1)
        if found:
            sbx = Sandbox.connect(found[0]["sandbox_id"], client=client)
            if found[0].get("state") == "paused":
                sbx.resume()
            return sbx
        sbx = Sandbox.create(
            "user-env", client=client, spend_cap=2, timeout_ms=60 * 60_000,
            metadata={"user": user_id}, allow_hosts=["pypi.org", "files.pythonhosted.org"],
        )
        sbx.set_auto_pause(300)
        return sbx

def run_code(user_id: str, code: str) -> str:
    sbx = sandbox_for(user_id)
    sbx.files.write("/tmp/main.py", code)
    result = sbx.commands.run("python3 /tmp/main.py", timeout_ms=60_000, throw_on_exit=False)
    return result.stdout if result.exit_code == 0 else result.stdout + result.stderr
```

Three limits stack, and each stops a different kind of runaway. A command's `timeoutMs` ends one script that never
finishes. The sandbox's `spendCap` holds what one user's sandbox may spend; a sandbox that reaches it is paused with
its files kept, and resumes only under a higher cap. The project's monthly cap holds everything the backend starts.

The `timeoutMs` on the sandbox counts running time only, since time spent paused does not count, so a sandbox that
sleeps between messages lasts for many sessions. Extend an active one with `setTimeout` (`set_timeout` in Python).

`allowHosts` puts the sandbox behind an outbound allow-list: here, the Python package index, so a user can still
install a package. Refused connections are recorded, and `promigence sandbox network SID` lists them.

## 4. Keep your own keys out of users' sandboxes

If code in a user's sandbox calls a model API on your account, do not hand it your key. Store the key once and bind
it to the API's host; its value then stays out of the sandbox, and `--no-raw` makes sure it is never given to one:

```bash
promigence secrets set OPENAI_KEY --from-env OPENAI_API_KEY --host api.openai.com --no-raw
```

Give it to a user's sandbox by name, with `secrets: ["OPENAI_KEY"]` on `Sandbox.fork` in TypeScript or
`secrets=["OPENAI_KEY"]` on `Sandbox.create` in Python, and have the code send its requests to the address
`secrets set` prints for the host: those requests are authenticated for you. A stored secret is never written into a
snapshot you build.

## 5. Clean up

When a user deletes their account, end their sandbox with `kill()` (or `promigence sandbox kill SID`). A paused
sandbox is kept for 30 days, and resuming it starts the 30 days again; one that reaches 30 days is deleted with its
files, and the organization's owner gets an email 7 days and 1 day before. Keep anything that must last longer in
your own storage.

`promigence project list` shows what each project spent this month. A paused sandbox does not count against your
plan's concurrency limit, so the idle rule decides how many users fit: 20 sandboxes at once on a new account, up to
100 on Pro, and up to 500 or 1,000 on Max.

## At a glance

| | Self-host | Managed, on Promigence |
|---|---|---|
| Isolation for code nobody reviewed | You choose the boundary, harden it and keep it patched | No sandbox can see, reach or address another, yours or anyone else's |
| A sandbox per user | You build the pools and the scheduling | `Sandbox.fork` or `Sandbox.create` per user, found again by label |
| Idle users | Machines cost money unless you build pausing | An idle rule pauses a sandbox; paused time is not billed, and it is kept 30 days |
| Spend | You build the metering and the limits | A spend cap per sandbox, a monthly cap per project, a quote before any run |
| Network | You write the egress rules | Full outbound, none, or an outbound allow-list, with refused hosts recorded |
| Secrets | Your vault and your injection | Stored secrets given only to the commands that name them, kept out of snapshots |
| Where the data lives | Your own cloud account | Promigence's service; your own cloud account is a conversation with Promigence |
| Cost | Machines plus engineering time | Per second: $0.17 an hour for a small sandbox while it runs |

**A sandbox per user**

A user's message reaches your backend, which finds that user's sandbox by its label or starts one from the user-env snapshot. The code runs in the user's own sandbox, its output goes back through your agent to the user, and the sandbox pauses itself when the user goes idle.

Steps:
1. Your users
2. Your backend: key with run access, project with a monthly cap
3. Find the user's sandbox by label, or start one from user-env
4. One sandbox per user, paused when idle
5. Output of the user's code

Connections:
1. Your users → Your backend: key with run access, project with a monthly cap: a message
2. Your backend: key with run access, project with a monthly cap → Find the user's sandbox by label, or start one from user-env: user id
3. Find the user's sandbox by label, or start one from user-env → One sandbox per user, paused when idle: connect, resume or fork
4. One sandbox per user, paused when idle → Output of the user's code: commands.run
5. Output of the user's code → Your users: the agent's answer

_Your users, your backend, one labelled sandbox each_

## FAQ

### Should I self-host or use a managed sandbox service for my agent product?

Use a managed service unless your users' data must stay in your own cloud account, you already run isolation for
untrusted code, or your sandboxes are busy around the clock at a scale where owned machines cost less. Self-hosting
means building isolation, fast starts, pausing, spend limits and egress rules yourself, then running them. With
Promigence those features come with the service, and running it in your own cloud account is arranged by conversation,
not self-serve.

### How do I stop one user from running up my bill?

Stack three limits: a timeout on each command, a spend cap on each user's sandbox, and a monthly cap on the project
your backend's key belongs to. A sandbox that reaches its cap is paused with its files kept, and a project at its
monthly cap refuses new work.

### Can one user's sandbox reach another user's?

No. No Promigence sandbox can see, reach or address another, yours or anyone else's. Give each user their own
sandbox, as above, and never reuse one across users.

### What happens to a user's files while they are away?

An idle rule pauses the sandbox, and it keeps its files. Paused time is not billed, and a paused sandbox is
kept for 30 days from its last pause. Resume it within that time and it comes back as it was.

### Can Promigence run in my own cloud account?

Not as a self-serve product today. If your data cannot leave your own cloud account, talk to Promigence about running
it there.

## Sources

- [A sandbox backend for agent-builder and workflow platforms](https://www.promigence.ai/for/agent-builders), read 17 Oct 2026
- [Code interpreters that answer while your user is still looking](https://www.promigence.ai/for/code-interpreters), read 17 Oct 2026
- [Limits and notices](https://www.promigence.ai/docs/limits): spend caps, paused sandboxes, project caps, data out, read 17 Oct 2026
- [Security](https://www.promigence.ai/security): isolation, secrets, your own cloud account, read 17 Oct 2026
- [Pricing](https://www.promigence.ai/#pricing): rates billed per second, plans and sandboxes at once, read 17 Oct 2026
- [Quickstart](https://www.promigence.ai/docs/quickstart): the build price, read 17 Oct 2026
- [FAQ](https://www.promigence.ai/faq): auto-pause, network limits, secrets, running in your own cloud account, read 17 Oct 2026
- [CLI reference](https://www.promigence.ai/docs/cli): `project create`, `project cap`, `keys create --access`, `secrets set --host`, `sandbox network`, `sandbox kill`, read 17 Oct 2026
- [Information for AI assistants](https://www.promigence.ai/ai-instructions): secrets bound to a host, projects with a spend cap each, read 17 Oct 2026
- [TypeScript SDK](https://www.promigence.ai/docs/sdk), and [@promigence/sdk on npm](https://www.npmjs.com/package/@promigence/sdk), version 0.1.0-beta.7, read 17 Oct 2026
- [promigence on PyPI](https://pypi.org/project/promigence/): the Python SDK, version 0.1.0b7, read 17 Oct 2026

Updated 2026-10-17.

