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.
Which apps have one
Section titled “Which apps have one”| 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. |
Use the database from a Worker
Section titled “Use the database from a Worker”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 DBWhat happens to the data
Section titled “What happens to the data”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.
Query the database
Section titled “Query the database”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:
jiayang db query acme/notes "SELECT id, author, body FROM notes ORDER BY id"id author body1 someone@example.com first note2 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:
jiayang db query acme/notes "SELECT * FROM notes" --limit 50 --offset 100jiayang db query acme/notes "UPDATE notes SET body = 'fixed' WHERE id = 7" --writejiayang db query acme/notes "SELECT * FROM notes" --csv -o notes.csvWho 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.
Run a SQL statement
Section titled “Run a SQL statement”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:
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.
Export the database
Section titled “Export the database”jiayang db export writes the whole database as a SQL dump that you can load into SQLite or another D1 database:
jiayang db export acme/notesExported the database of acme/notes to acme-notes.sql (48,213 bytes).The dump is plain SQL:
head -3 acme-notes.sqlPRAGMA 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:
jiayang db export acme/notes -o backup.sqljiayang db export acme/notes --stdout | sqlite3 notes.dbThe 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.
Next steps
Section titled “Next steps”- Backups and restore to put the database back as it was at a moment.
- Postgres to connect a database you already have.
- Versions and rollback to put earlier code back without touching the data.