- Backup and restore
Backup and restore
Last updated Oct 12, 2026 Note
This page documents Sandbox SDK 0.x for existing applications. For new applications, refer to Save and restore a sandbox with snapshots. To move an existing application to @cloudflare/sandbox 1.0, refer to Migrate from Sandbox SDK 0.x.
This guide shows you how to snapshot a sandbox directory to R2 and restore it later.
Use backup and restore when a project directory such as /workspace should come back after the sandbox sleeps. For a separate persisted storage path, mount a bucket instead. If you mount a bucket over /workspace, the mount overlays files seeded by your image in production.
For why production restore uses an overlay, refer to Directory backups.
Prerequisites
- Create an R2 bucket:
sh
npx wrangler r2 bucket create my-backup-bucket
- Add the
BACKUP_BUCKETR2 binding and presigned URL settings to your Wrangler configuration:
jsonc
{
"name": "my-sandbox-worker",
"main": "src/index.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-10-27",
"compatibility_flags": ["nodejs_compat"],
"containers": [
{
"class_name": "Sandbox",
"image": "./Dockerfile",
},
],
"durable_objects": {
"bindings": [
{
"class_name": "Sandbox",
"name": "Sandbox",
},
],
},
"migrations": [
{
"new_sqlite_classes": ["Sandbox"],
"tag": "v1",
},
],
"vars": {
"BACKUP_BUCKET_NAME": "my-backup-bucket",
"CLOUDFLARE_ACCOUNT_ID": "<YOUR_ACCOUNT_ID>",
},
"r2_buckets": [
{
"binding": "BACKUP_BUCKET",
"bucket_name": "my-backup-bucket",
},
],
}
```toml name = "my-sandbox-worker" main = "src/index.ts" upload_source_maps = true # Set this to today's date compatibility_date = "2026-10-27" compatibility_flags = [ "nodejs_compat" ]
[[containers]] class_name = "Sandbox" image = "./Dockerfile"
[[durable_objects.bindings]] class_name = "Sandbox" name = "Sandbox"
[[migrations]] new_sqlite_classes = [ "Sandbox" ] tag = "v1"
[vars]
BACKUP_BUCKET_NAME = "my-backup-bucket"
CLOUDFLARE_ACCOUNT_ID = "
[[r2_buckets]] binding = "BACKUP_BUCKET" bucket_name = "my-backup-bucket" ```
If the bucket uses a jurisdiction-specific endpoint, add BACKUP_BUCKET_ENDPOINT to vars. For an EU bucket, use https://<ACCOUNT_ID>.eu.r2.cloudflarestorage.com.
3. Store R2 API credentials as secrets:
sh
npx wrangler secret put R2_ACCESS_KEY_ID
npx wrangler secret put R2_SECRET_ACCESS_KEY
Create the token in the Cloudflare dashboard ↗︎ under R2 > Overview > Manage R2 API Tokens. Grant Object Read & Write on the backup bucket.
Note
The vars and API secrets in steps 2 and 3 are required for production. For wrangler dev, only the BACKUP_BUCKET binding is required. Refer to Use backup and restore in local development.
Create a backup
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });
The directory must be an absolute path under /workspace, /home, /tmp, /var/tmp, or /app.
Restore a backup
Stop processes that write to the target directory, then restore:
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });
const result = await sandbox.restoreBackup(backup);
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });
const result = await sandbox.restoreBackup(backup);
In production, restore mounts a copy-on-write overlay. The mount is lost when the sandbox sleeps or the container restarts. Restore again from the stored handle.
The restore target is backup.dir. You can point that field at a different allowed directory than the one you originally backed up.
Exclude generated caches
After a production restore, renaming a directory inside the restored tree can fail with EXDEV (cross-device link not permitted). Omit disposable generated directories from the backup, or delete them after restore. Vite's cache is one such directory:
const backup = await sandbox.createBackup({
dir: "/workspace/app",
excludes: ["node_modules/.vite"],
});
const backup = await sandbox.createBackup({
dir: "/workspace/app",
excludes: ["node_modules/.vite"],
});
await sandbox.restoreBackup(backup);
await sandbox.exec("rm -rf /workspace/app/node_modules/.vite");
await sandbox.restoreBackup(backup);
await sandbox.exec("rm -rf /workspace/app/node_modules/.vite");
This failure does not occur in wrangler dev, which extracts the archive. For overlay restore, refer to Directory backups.
Exclude gitignored files
To skip .gitignore matches such as node_modules/ or dist/ in a git repository:
const backup = await sandbox.createBackup({
dir: "/workspace",
gitignore: true,
});
const backup = await sandbox.createBackup({
dir: "/workspace",
gitignore: true,
});
If the directory is not inside a git repository, gitignore has no effect. If git is not installed in the container, the SDK logs a warning and continues without git-based exclusions. Nested .gitignore files apply.
Checkpoint and roll back
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });
try {
await sandbox.exec("npm install <some-experimental-package>");
await sandbox.exec("npm run build");
} catch (error) {
await sandbox.restoreBackup(checkpoint);
}
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });
try {
await sandbox.exec("npm install <some-experimental-package>");
await sandbox.exec("npm run build");
} catch (error) {
await sandbox.restoreBackup(checkpoint);
}
Store backup handles
DirectoryBackup is serializable. Persist it to KV, D1, or Durable Object storage:
const backup = await sandbox.createBackup({
dir: "/workspace",
name: "deploy-v2",
ttl: 604800, // 7 days
});
await env.KV.put(`backup:${userId}`, JSON.stringify(backup));
const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
await sandbox.restoreBackup(JSON.parse(stored));
}
const backup = await sandbox.createBackup({
dir: "/workspace",
name: "deploy-v2",
ttl: 604800, // 7 days
});
await env.KV.put(`backup:${userId}`, JSON.stringify(backup));
const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
await sandbox.restoreBackup(JSON.parse(stored));
}
Set a name and TTL
Names can be up to 256 characters. The default TTL is 3 days (259200 seconds). The SDK rejects an expired backup at restore time. It does not delete the R2 objects.
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const shortBackup = await sandbox.createBackup({
dir: "/workspace",
ttl: 600, // 10 minutes
});
const longBackup = await sandbox.createBackup({
dir: "/workspace",
name: "daily-snapshot",
ttl: 604800, // 7 days
});
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const shortBackup = await sandbox.createBackup({
dir: "/workspace",
ttl: 600, // 10 minutes
});
const longBackup = await sandbox.createBackup({
dir: "/workspace",
name: "daily-snapshot",
ttl: 604800, // 7 days
});
To delete expired objects automatically, add an R2 object lifecycle rule on the backups/ prefix. If your longest TTL is 7 days, expire objects older than 7 days.
Clean up backup objects
Archives live at backups/{backupId}/data.sqsh and backups/{backupId}/meta.json.
Replace the latest backup
if (previousBackup) {
await env.BACKUP_BUCKET.delete([
`backups/${previousBackup.id}/data.sqsh`,
`backups/${previousBackup.id}/meta.json`,
]);
}
const backup = await sandbox.createBackup({
dir: "/workspace",
name: "latest",
});
await env.KV.put("latest-backup", JSON.stringify(backup));
if (previousBackup) {
await env.BACKUP_BUCKET.delete([
`backups/${previousBackup.id}/data.sqsh`,
`backups/${previousBackup.id}/meta.json`,
]);
}
const backup = await sandbox.createBackup({
dir: "/workspace",
name: "latest",
});
await env.KV.put("latest-backup", JSON.stringify(backup));
Delete a backup by ID
await env.BACKUP_BUCKET.delete([
`backups/${backup.id}/data.sqsh`,
`backups/${backup.id}/meta.json`,
]);
await env.BACKUP_BUCKET.delete([
`backups/${backup.id}/data.sqsh`,
`backups/${backup.id}/meta.json`,
]);
Delete backups by age
List objects under backups/ and delete by upload time:
const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
for (const object of listed.objects) {
const ageMs = Date.now() - object.uploaded.getTime();
if (ageMs > sevenDaysMs) {
await env.BACKUP_BUCKET.delete(object.key);
}
}
const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
for (const object of listed.objects) {
const ageMs = Date.now() - object.uploaded.getTime();
if (ageMs > sevenDaysMs) {
await env.BACKUP_BUCKET.delete(object.key);
}
}
Use backup and restore in local development
Pass localBucket: true so wrangler dev uses the BACKUP_BUCKET binding. Presigned URL credentials are not required.
const backup = await sandbox.createBackup({
dir: "/workspace",
localBucket: Boolean(env.LOCAL_DEV),
});
const result = await sandbox.restoreBackup(backup);
const backup = await sandbox.createBackup({
dir: "/workspace",
localBucket: Boolean(env.LOCAL_DEV),
});
const result = await sandbox.restoreBackup(backup);
Local restore extracts the archive with unsquashfs and replaces the directory. The stored handle's localBucket field selects the restore path.
Fix path permissions
createBackup() must read every file under the target directory. Files with mode 0600 or directories owned by another user cause BackupCreateError.
Set permissions in the image when you can. a+rX adds read permission on files and execute permission on directories:
RUN mkdir -p /home/sandbox && chmod -R a+rX /home/sandbox
If a process creates restrictive files at runtime, fix them before the backup:
await sandbox.exec("chmod -R a+rX /home/sandbox/.claude");
const backup = await sandbox.createBackup({ dir: "/home/sandbox" });
Handle errors
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
try {
const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
if (error.code === "INVALID_BACKUP_CONFIG") {
console.error("Configuration error:", error.message);
} else if (error.code === "BACKUP_CREATE_FAILED") {
console.error("Backup failed:", error.message);
}
}
try {
await sandbox.restoreBackup(backup);
} catch (error) {
if (error.code === "BACKUP_NOT_FOUND") {
console.error("Backup not found in R2:", error.message);
} else if (error.code === "BACKUP_EXPIRED") {
console.error("Backup TTL has elapsed:", error.message);
} else if (error.code === "BACKUP_RESTORE_FAILED") {
console.error("Restore failed:", error.message);
}
}
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
try {
const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
if (error.code === "INVALID_BACKUP_CONFIG") {
console.error("Configuration error:", error.message);
} else if (error.code === "BACKUP_CREATE_FAILED") {
console.error("Backup failed:", error.message);
}
}
try {
await sandbox.restoreBackup(backup);
} catch (error) {
if (error.code === "BACKUP_NOT_FOUND") {
console.error("Backup not found in R2:", error.message);
} else if (error.code === "BACKUP_EXPIRED") {
console.error("Backup TTL has elapsed:", error.message);
} else if (error.code === "BACKUP_RESTORE_FAILED") {
console.error("Restore failed:", error.message);
}
}
Related resources
- Directory backups - Overlay restore, local extract, and
EXDEV - Backups API - Methods, options, and types
- Storage API - Mount S3-compatible buckets
- R2 documentation - R2 buckets and credentials
- R2 lifecycle rules - Automatic object cleanup
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/sdk/guides/backup-restore/#page","headline":"Backup and restore","description":"Snapshot a sandbox directory to R2 and restore it later with the Sandbox SDK 0.x.","url":"https://developers.cloudflare.com/sandbox/sdk/guides/backup-restore/","inLanguage":"en","image":"https://developers.cloudflare.com/sandbox/sdk/guides/backup-restore/og.png?v=cdde19a33693cf6d","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Connect browser-based terminals to Sandbox SDK 0.x shells using xterm.js or raw WebSockets. title: Browser terminals image: https://developers.cloudflare.com/sandbox/sdk/guides/browser-terminals/og.png?v=ab0e05afadf877f5
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt
Use this file to discover all available pages before exploring further.
Docs