Skip to content

Database

Every Worker app gets its own SQL database: a Cloudflare D1 database, which is SQLite, bound to your code as DB. It’s made on the app’s first deploy, belongs to the app rather than a version, and only that app can reach it.

Kind Database
Worker, including a framework build Yes, from its first deploy.
Static site No.
Container No.

env.DB is a D1 database. Your code creates its own tables:

export default {
async fetch(request, env) {
await env.DB.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, author TEXT, body TEXT)");
if (request.method === "POST") {
await env.DB.prepare("INSERT INTO notes (author, body) VALUES (?, ?)")
.bind("someone@example.com", await request.text())
.run();
}
const { results } = await env.DB.prepare("SELECT author, body FROM notes ORDER BY id").all();
return Response.json(results);
},
};

In a real app, take the author from the caller the SDK verified rather than from the request.

In TypeScript, declare it on your Env:

interface Env {
DB: D1Database;
}

A wrangler config may name the binding, for your own tools’ sake, as long as it’s called DB. The platform binds the app’s own database there whatever the config’s database_id says. Any other name is refused:

error: wrangler.jsonc binds D1 as MAIN_DB; the platform gives each app one database, bound as DB.
  • Deploys and rollbacks leave it alone. Every version of the app talks to the same database, so rolling back changes the code, not the data.
  • Deleting the app deletes the database with it, straight away. Nothing is restorable, so export it first.

The database counts toward the workspace’s storage, with the site files its apps serve: 1 GB on Free, 10 GB on Team and 100 GB on Business, for everything together. It’s measured about once an hour.

Writes are never refused for it. Once the workspace is past its storage, though, nothing new deploys in it until it’s back under: delete rows, or move to a bigger plan. Cloudflare’s own limit is 10 GB for one database.

The workspace’s databases together also get a limit of their own, inside the storage: 1 GB on Free, 2 GB on Team and 10 GB on Business. They’re measured every few minutes. Over the limit, apps with a database answer only the workspace’s owners and admins, with 507 for everyone else, and past 10% over they answer no one. On Free, a workspace that stays over is suspended after 7 days, and its largest databases are deleted 30 days after that, after email notice. Limits has the whole of it.

Free also stops for the month at 50,000,000 rows read or 100,000 written across its databases. Cloudflare counts a row for every row a query scans, not only the ones it returns, so an index on the columns you filter by keeps the count down.

jiayang db exec runs one SQL statement against an app’s database. An owner can run any statement; an admin can run a DELETE, a DROP TABLE or a DROP INDEX, since a read sees everything in the database and only an owner may export it:

Terminal window
jiayang db exec acme/notes "DELETE FROM notes WHERE created < '2026-01-01'"

It takes one statement at a time, and each run goes in the audit log. While the workspace’s databases are over its limit, or a Free workspace is paused for its database rows, it takes only a DELETE, a DROP TABLE or a DROP INDEX from anyone, so you can get back under, and the database is measured again straight after. The log records the kind of statement and a hash of it, never its text.

Rows a statement reads and writes count toward Free’s monthly rows like any other query. Whether a DELETE shrinks the database’s size at once is up to D1; if the size doesn’t come down, export what you want to keep, delete the app’s data, and load it back.

jiayang db export writes the whole database as a SQL dump, ready to load into SQLite or another D1 database:

Terminal window
jiayang db export acme/notes
Exported acme/notes's database to acme-notes.sql (48213 bytes).

The dump is plain SQL:

Terminal window
head -3 acme-notes.sql
PRAGMA defer_foreign_keys=TRUE;
CREATE TABLE notes (id INTEGER PRIMARY KEY, author TEXT, body TEXT);
INSERT INTO "notes" ("id","author","body") VALUES(1,'someone@example.com','first note');

-o names the file, and --stdout writes the dump to stdout instead:

Terminal window
jiayang db export acme/notes -o backup.sql
jiayang db export acme/notes --stdout | sqlite3 notes.db

The file is written beside its target and moved into place once the dump is whole, so a failed export never leaves a file that looks complete.

The dashboard has the same export under the app’s Settings tab, as Database export.

Only the workspace’s owners can export a database. Anyone else is refused:

error: You don't have permission to do that.

One export of an app runs at a time:

error: This app's database is being exported already. Try again in a minute.

A static site, a container, or a Worker app that hasn’t been deployed yet has nothing to export:

error: This app has no database to export.

Each export goes in the audit log.