diff --git a/src/content/docs/sandbox/guides/session-restoration.mdx b/src/content/docs/sandbox/guides/session-restoration.mdx new file mode 100644 index 00000000000..fd8b4b5f9c9 --- /dev/null +++ b/src/content/docs/sandbox/guides/session-restoration.mdx @@ -0,0 +1,346 @@ +--- +title: Restore sessions +pcx_content_type: how-to +sidebar: + order: 12 +description: Reconnect to sessions across requests and restore session state after container restarts. +--- + +import { TypeScriptExample } from "~/components"; + +This guide covers patterns for maintaining and recovering session state across Worker requests and container restarts. + +Sandbox containers sleep after a period of inactivity (10 minutes by default). When a container restarts, all in-memory state is lost: shell sessions reset, processes terminate, and files written to the container filesystem are deleted. This guide shows you how to design applications that handle this gracefully. + +## Reconnect to a session across requests + +Sessions persist within a single container lifetime. Use `getSession()` to reconnect to an existing session from a new Worker request without losing its shell state, working directory, or environment variables: + + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +export { Sandbox } from "@cloudflare/sandbox"; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const sandbox = getSandbox(env.Sandbox, "user-123"); + + if (url.pathname === "/init") { + const session = await sandbox.createSession({ + id: "workspace", + env: { NODE_ENV: "development" }, + cwd: "/workspace", + }); + await session.exec("git clone https://github.com/user/repo.git ."); + await session.exec("npm install"); + return Response.json({ status: "initialized" }); + } + + if (url.pathname === "/build") { + // Reconnect to the existing session — env and cwd are preserved + const session = await sandbox.getSession("workspace"); + const result = await session.exec("npm run build"); + return Response.json({ output: result.stdout, success: result.success }); + } + + return new Response("Not found", { status: 404 }); + }, +}; +``` + + + +`getSession()` returns a session handle bound to the named session. If the container is still active, the session retains its shell state from the previous request. + +:::note +`getSession()` does not create a new session if the ID does not exist. If you reference a session that was never created or was deleted, subsequent commands run in a fresh shell with default settings. Use `createSession()` to explicitly set environment variables and working directory. +::: + +## Detect container restarts + +Because container state is ephemeral, check for the presence of expected files or processes before assuming the environment is already initialized: + + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +async function ensureInitialized(sandbox: ReturnType) { + const files = await sandbox.listFiles("/workspace"); + const isInitialized = files.some((f) => f.name === "node_modules"); + + if (!isInitialized) { + // Container restarted — reinitialize the environment + await sandbox.exec("npm install"); + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, "user-123"); + await ensureInitialized(sandbox); + + const result = await sandbox.exec("npm test"); + return Response.json({ output: result.stdout }); + }, +}; +``` + + + +## Persist and restore session configuration + +Session configuration is held in memory and lost when the container restarts. Store session metadata in [Workers KV](/kv/) or [D1](/d1/) and reapply it when recreating a session: + + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +interface SessionConfig { + id: string; + env: Record; + cwd: string; +} + +async function getOrRestoreSession( + sandbox: ReturnType, + config: SessionConfig, + kv: KVNamespace, +) { + const key = `session-config:${config.id}`; + + // Persist the session config so it can be restored after a restart + await kv.put(key, JSON.stringify(config)); + + return sandbox.createSession({ + id: config.id, + env: config.env, + cwd: config.cwd, + }); +} + +async function restoreSessionFromKV( + sandbox: ReturnType, + sessionId: string, + kv: KVNamespace, +) { + const stored = await kv.get(`session-config:${sessionId}`); + if (!stored) { + throw new Error(`No stored config found for session: ${sessionId}`); + } + + const config: SessionConfig = JSON.parse(stored); + + return sandbox.createSession({ + id: config.id, + env: config.env, + cwd: config.cwd, + }); +} +``` + + + +## Restore filesystem state with backups + +Use [backup and restore](/sandbox/guides/backup-restore/) to persist filesystem state across container restarts. Store the backup handle in KV and re-restore it on startup: + + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +const BACKUP_KEY = "workspace-backup"; + +async function saveWorkspace( + sandbox: ReturnType, + kv: KVNamespace, +) { + const backup = await sandbox.createBackup({ + dir: "/workspace", + useGitignore: true, + ttl: 604800, // 7 days + }); + await kv.put(BACKUP_KEY, JSON.stringify(backup)); + return backup; +} + +async function restoreWorkspace( + sandbox: ReturnType, + kv: KVNamespace, +) { + const stored = await kv.get(BACKUP_KEY); + if (!stored) return false; + + const backup = JSON.parse(stored); + await sandbox.restoreBackup(backup); + return true; +} +``` + + + +:::caution[Production only] +Backup and restore requires FUSE support and does not work with `wrangler dev`. Deploy your Worker with `wrangler deploy` to use this feature. Refer to the [backup and restore guide](/sandbox/guides/backup-restore) for the required Wrangler configuration. +::: + +## Full session restoration pattern + +Combine session configuration persistence with filesystem backups for a complete restoration pattern. This pattern saves and restores both shell configuration and workspace files: + + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +export { Sandbox } from "@cloudflare/sandbox"; + +interface SessionState { + sessionConfig: { + env: Record; + cwd: string; + }; + backupHandle: object | null; +} + +const STATE_KEY = "session-state:user-123"; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const sandbox = getSandbox(env.Sandbox, "user-123"); + + if (url.pathname === "/start") { + return handleStart(sandbox, env); + } + + if (url.pathname === "/resume") { + return handleResume(sandbox, env); + } + + if (url.pathname === "/save") { + return handleSave(sandbox, env); + } + + return new Response("Not found", { status: 404 }); + }, +}; + +async function handleStart(sandbox, env) { + // Create a session with the desired configuration + const session = await sandbox.createSession({ + id: "main", + env: { NODE_ENV: "development", PORT: "3000" }, + cwd: "/workspace", + }); + + await session.exec("git clone https://github.com/user/repo.git ."); + await session.exec("npm install"); + + // Persist the session config for future restores + const state: SessionState = { + sessionConfig: { + env: { NODE_ENV: "development", PORT: "3000" }, + cwd: "/workspace", + }, + backupHandle: null, + }; + await env.KV.put(STATE_KEY, JSON.stringify(state)); + + return Response.json({ status: "started" }); +} + +async function handleResume(sandbox, env) { + const stored = await env.KV.get(STATE_KEY); + if (!stored) { + return Response.json({ error: "No saved state found" }, { status: 400 }); + } + + const state: SessionState = JSON.parse(stored); + + // Check whether the container is still active by probing a known file + const files = await sandbox.listFiles("/workspace"); + const isActive = files.some((f) => f.name === "package.json"); + + if (!isActive && state.backupHandle) { + // Container restarted — restore filesystem from backup + await sandbox.restoreBackup(state.backupHandle); + } + + // Recreate the session with stored configuration + const session = await sandbox.createSession({ + id: "main", + env: state.sessionConfig.env, + cwd: state.sessionConfig.cwd, + }); + + const result = await session.exec("node --version"); + return Response.json({ status: "resumed", output: result.stdout }); +} + +async function handleSave(sandbox, env) { + const stored = await env.KV.get(STATE_KEY); + const state: SessionState = stored ? JSON.parse(stored) : { sessionConfig: {}, backupHandle: null }; + + // Snapshot the workspace and update the stored state + const backup = await sandbox.createBackup({ + dir: "/workspace", + useGitignore: true, + ttl: 604800, + }); + + state.backupHandle = backup; + await env.KV.put(STATE_KEY, JSON.stringify(state)); + + return Response.json({ status: "saved", backupId: backup.id }); +} +``` + + + +## Re-run startup commands after restart + +For sandboxes that run background services, re-run startup commands after detecting a container restart: + + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +async function ensureServicesRunning(sandbox: ReturnType) { + const processes = await sandbox.listProcesses(); + const serverRunning = processes.some((p) => p.command.includes("node server.js")); + + if (!serverRunning) { + // Server is not running — start it + await sandbox.startProcess("node /workspace/server.js", { + id: "web-server", + env: { PORT: "3000" }, + }); + } +} +``` + + + +## Best practices + +- **Use `getSession()` for cross-request continuity** — Within a container lifetime, `getSession()` reconnects to an existing shell context without resetting state. +- **Persist session config externally** — Store environment variables and working directory in KV or D1 so you can recreate sessions with the correct configuration after a restart. +- **Probe before assuming state** — Check for expected files or processes before using the sandbox to detect whether the container restarted. +- **Save backups before long pauses** — If users step away, save a backup so work is not lost when the container sleeps. +- **Re-restore backups after restart** — FUSE mounts are ephemeral. After a container restart, call `restoreBackup()` again using the stored handle. +- **Use `keepAlive` for interactive sessions** — Set [`keepAlive: true`](/sandbox/configuration/sandbox-options/#keepalive) on long-running interactive environments to prevent the container from sleeping during active use. +- **Clean up stored state** — Delete KV entries and backup objects when the sandbox is destroyed to avoid stale data. + +## Related resources + +- [Session management](/sandbox/concepts/sessions/) — How sessions and shell state work +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) — Container states, inactivity sleep, and restart behavior +- [Backup and restore](/sandbox/guides/backup-restore/) — Snapshot and restore filesystem directories +- [Sessions API](/sandbox/api/sessions/) — `createSession()`, `getSession()`, and `deleteSession()` reference +- [Sandbox options](/sandbox/configuration/sandbox-options/) — Configure `sleepAfter` and `keepAlive` diff --git a/src/content/docs/sandbox/index.mdx b/src/content/docs/sandbox/index.mdx index 0b405e3ac73..6d02fe4eb69 100644 --- a/src/content/docs/sandbox/index.mdx +++ b/src/content/docs/sandbox/index.mdx @@ -254,6 +254,12 @@ Monitor files and directories for changes using native filesystem events. Perfec + + +Reconnect to shell sessions across Worker requests and recover session configuration and filesystem state after container restarts using backups and external storage. + + + Keep credentials in your Worker while allowing sandboxes to access external APIs. A Worker proxy validates short-lived JWT tokens from the sandbox and injects real credentials at request time.