Skip to content

Database

Every Worker app gets its own SQL database, a Cloudflare D1 database (SQLite), bound to your code as DB. Container apps get one too, reached over HTTP through storage. It’s created on the app’s first deploy, near where the app was created from (see storage), and belongs to the app, not to a version. No other app can reach it. Databases are on every plan.

To use a Postgres database you already have instead, or as well, see Postgres.

Kind Database
Worker, including a framework build Yes, from its first deploy.
Static site No.
Container Yes, from its first deploy, reached over HTTP. See storage.

env.DB is a D1 database. Your code creates the tables it needs:

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, not from the request.

In TypeScript, declare it on your Env:

interface Env {
DB: D1Database;
}

A wrangler config may declare the binding for your own tools, as long as it’s named DB. We bind the app’s own database there, whatever the config’s database_id says. Any other name is refused:

error: wrangler.jsonc asks for things we don't offer:
d1_databases (MAIN_DB): each app gets one database, bound as DB

Deploys and rollbacks don’t touch the database. Every version of the app uses the same one, so rolling back changes the code and leaves the data as it is.

A workspace owner can put the database back as it was at a minute in the last 30 days, or after a restore, to a bookmark. See Restore an app’s database to a moment.

A deleted app’s database is kept with it for 30 days, and comes back if the app is restored: see Backups and restore. Deleting the app for good deletes its database at once, and it can’t be restored, so export it first.

The database counts toward the workspace’s storage, together with the site files its apps serve and the apps’ own files: 1 GB on Free, 10 GB on Team and 100 GB on Business. Each database counts at its size when we last measured it, about once an hour.

Going over storage blocks new deploys in the workspace until it’s back under. Once any app in it keeps files of its own, every app with a place for files stops answering too, so their databases take no more writes. Delete rows or files, or move to a bigger plan. Cloudflare limits a single database to 10 GB.

The workspace’s databases together also have their own limit, inside the storage: 1 GB on Free, 2 GB on Team and 10 GB on Business. We measure them every few minutes. Over the limit, apps with a database answer only the workspace’s owners and admins, and return 507 to everyone else. More than 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, with notice by email. See Limits for details.

On Free, apps stop for the month at 50,000,000 rows read or 100,000 rows written across the workspace’s databases. Team includes 500,000,000 rows read and 500,000 written each month, and Business ten times that. Past that, reads cost $2.50 per billion and writes $2.25 per million.

We bill only Cloudflare’s exact figures. Databases over the plan’s database limit cost $1.55 per GB-month, charged by the hour for as long as they stay over. Cloudflare counts every row a query scans, including rows it doesn’t return, so add an index on the columns you filter by to keep the count down.

Plans and billing has the rest.

A workspace owner can look at an app’s database, and change it, from the dashboard or the CLI. Open the app and pick the Database tab. It’s there for the workspace’s owners and admins, on an app that has a database.

The tab lists the app’s tables. Click one to see its rows, or write a statement and press Run, or Ctrl+Enter (⌘+Enter on a Mac). Results come 100 rows at a time, with Next and Previous to page through them. Download CSV saves up to 10,000 rows of the query, and up to 32 MB, as a CSV file.

The console is read only until you pick Write. Read only runs a SELECT, a WITH that ends in a SELECT, VALUES, an EXPLAIN of one of those, or one of these PRAGMAs: table_list, table_info, table_xinfo, index_list, index_info, index_xinfo, foreign_key_list and foreign_key_check. Anything else is refused before it reaches the database. Should a statement read only lets through change the database all the same, the query fails rather than show its rows, and the log has it as a write. In write mode, the statement runs as written, once, and the dashboard asks you to confirm each run first. There’s no undo, but an owner can restore the database to a minute in the last 30 days.

jiayang db query does the same from a terminal:

Terminal window
jiayang db query acme/notes "SELECT id, author, body FROM notes ORDER BY id"
id author body
1 someone@example.com first note
2 someone@example.com second note
Rows 1 to 2. 2 rows read.

--limit sets how many rows come back, up to 1,000, and --offset skips rows to show the next page. --write runs a statement that changes the database. --csv prints up to 10,000 rows as CSV instead of a table, and -o saves them to a file:

Terminal window
jiayang db query acme/notes "SELECT * FROM notes" --limit 50 --offset 100
jiayang db query acme/notes "UPDATE notes SET body = 'fixed' WHERE id = 7" --write
jiayang db query acme/notes "SELECT * FROM notes" --csv -o notes.csv

Who can do what is the same as for jiayang db exec below. A workspace owner can read and write. A workspace admin can only delete, in write mode, and sees no rows. Nobody else can query an app’s database, including the app’s own owners and editors who aren’t workspace owners or admins. While the workspace’s databases are over the limit, only deletes run, in write mode, and nothing reads.

A query shows the values as they are. NULL shows as NULL, and a BLOB as hex, like x'00ff'. In a CSV, text that starts with =, +, -, @, a tab or a carriage return gets a ' in front, so a spreadsheet shows it as text rather than running it as a formula.

Each page is a query of its own. We send a query to the database as a subquery that reads only that page, so a big table isn’t read whole, but D1 still counts every row it scans, and those count toward the workspace’s database rows. Filter with WHERE rather than paging through a whole table, and add an index on what you filter by.

We wait 15 seconds for an answer. A read that takes longer is stopped waiting for, and says so. A write that takes longer may still have run, so check before you run it again. D1 itself stops any query after 30 seconds. A page can be up to 8 MB of results; ask for fewer rows or fewer columns if it’s more. A page can start up to 100,000 rows in. To see more than that, narrow the query or export the database.

Queries share a budget with jiayang files: a workspace can make 30 a minute, and each person 20 a minute. They also count toward the workspace’s 600 calls an hour to its apps’ databases, with jiayang db exec, restores and exports: a query that reads is 1, a statement that writes 2, and the last 60 are kept for restores (see limits). Past either, a query is refused with 429 and says how many seconds to wait.

In the dashboard, reading the database needs a sign-in from the last hour, and write mode one from the last 15 minutes. If yours is older, the dashboard asks you to sign in again and brings you back.

The audit log records every query, read only or not, as app.database_exec, with the kind of statement and a hash of it, never its text. Each page is its own entry.

jiayang db exec runs one SQL statement against an app’s database. A workspace owner can run any statement. A workspace admin can run only a DELETE, a DROP TABLE or a DROP INDEX, because a read would show everything in the database, and only owners may export it:

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

It runs one statement at a time. The audit log records each run as the kind of statement and a hash of it, never its text. A refused statement is recorded the same way, as app.database_exec_refused. While the workspace is suspended, nothing runs, not even a SELECT, and every statement is recorded as refused.

While the workspace’s databases are over the limit, or a Free workspace is paused for its database rows, everyone is limited to DELETE, DROP TABLE and DROP INDEX, so you can get back under. We measure the database again right after each one.

A statement limited to deleting can’t read anything back. It returns no rows, and it can’t have a RETURNING clause, a SELECT, VALUES or WITH anywhere in it, or an IN that names a table.

In its WHERE it can use CASE, CAST, the usual operators, and only these functions: date, time, datetime, julianday, unixepoch, strftime, lower, upper, length, substr, substring, trim, ltrim, rtrim, coalesce, ifnull, nullif, iif, typeof, instr, round, min, max, likely, unlikely, hex, unicode, sign and random.

Other functions, MATCH, REGEXP, ESCAPE, -> and ->> are refused, because their error messages can show what’s in the database. A delete still says how many rows it removed, so an admin who deletes rows by a condition learns that some matched it.

On Free, the rows a statement reads and writes count toward the month’s rows like any other query. The exception is while the workspace’s databases are over the limit. Then deletes don’t count, including the one that brings you back under, up to 1,000,000 rows written and 500,000,000 read a month. A workspace that moved to Free holding more than Free allows gets more.

Free’s database rows has the details. A delete made through the app counts as usual. D1 decides whether a DELETE shrinks the database right away. 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 that you can load into SQLite or another D1 database:

Terminal window
jiayang db export acme/notes
Exported the database of acme/notes to acme-notes.sql (48,213 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 dump goes to a temporary file next to the target and is moved into place when it’s complete, so a failed export never leaves a partial file behind that looks complete.

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

Only workspace owners can export a database. Anyone else gets:

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

Only one export of an app can run at a time:

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

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

error: This app has no database to export.

Each export is recorded in the audit log.