D1
SQL
Cloudflare D1
sqlite • free • edge

Cloudflare D1 Database: when should you use it?

Cloudflare D1 is serverless managed SQLite for Workers and Pages, useful for small side projects but limited if you later need MySQL/PostgreSQL.

10 min read16/06/2026

The problem D1 Database solves

You have a side project, internal tool, or personal app running on Cloudflare Workers or Pages and need somewhere to store data. The usual options — PlanetScale, Supabase, Turso — all have free tiers but require extra setup and come with connection or bandwidth limits.

Cloudflare D1 Database is a managed SQLite that runs directly on the edge, built into the Workers and Pages ecosystem. No connection pooling to configure, no latency to a database in a remote region — queries run as close to the user as possible.

What D1 Database is, briefly

D1 is Cloudflare's managed SQLite database, distributed across the edge network. It is not MySQL, not PostgreSQL — it is SQLite, with all the advantages and trade-offs that come with it.

You access it through a binding in Cloudflare Workers or Pages Functions — there's no connection string exposed to the outside world like a traditional database.

Free tier

Check the official cloudflare.com/developer-platform/d1 page for current figures before building on it.

Workers Free Workers Paid
Storage 5GB 10GB ($0.75/GB beyond)
Rows read/day 5 million 25 billion/month
Rows written/day 100,000 50 million/month
Number of databases Unlimited Unlimited
Price Free $5/month (Workers Paid)

The free tier is sufficient for a side project with moderate traffic. 5 million reads per day is generous for a personal app.

Basic setup

Create the database

# Install Wrangler if you haven't already
npm install -g wrangler
wrangler login

# Create a D1 database
wrangler d1 create my-database

The output returns a database_id — use it in wrangler.toml.

Configure the binding

# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

Create schema and run migrations

mkdir -p migrations
cat > migrations/0001_init.sql << 'EOF'
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  published INTEGER DEFAULT 0,
  FOREIGN KEY (user_id) REFERENCES users(id)
);
EOF

wrangler d1 migrations apply my-database

Querying in a Worker

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { pathname } = new URL(request.url);

    if (pathname === "/users") {
      const { results } = await env.DB.prepare(
        "SELECT id, email, name FROM users ORDER BY created_at DESC LIMIT 20"
      ).all();

      return Response.json(results);
    }

    if (pathname === "/users" && request.method === "POST") {
      const { email, name } = await request.json<{ email: string; name: string }>();

      const result = await env.DB.prepare(
        "INSERT INTO users (email, name) VALUES (?, ?)"
      )
        .bind(email, name)
        .run();

      return Response.json({ id: result.meta.last_row_id });
    }

    return new Response("Not found", { status: 404 });
  },
};

Local dev

# Run the Worker locally with a local D1 instance
wrangler dev --local

# Run SQL directly to inspect data
wrangler d1 execute my-database --local --command "SELECT * FROM users"

Real advantages

Zero latency to the database D1 replicates read replicas to every Cloudflare edge location. Read queries in a Worker run as close to the user as possible — no hop to a specific region.

No connection management Traditional databases have connection limits — you need a pool, and you worry about max connections under load. D1 bindings in Workers don't have a connection pool concept. Each Worker invocation handles itself.

Free tier never expires Unlike AWS RDS free tier (12 months), D1's free tier on the Workers Free plan has no expiry.

Native fit in the Cloudflare ecosystem If you're already using Workers, Pages, R2, or Queues — D1 plugs in naturally without additional infrastructure.

Limitations and drawbacks

SQLite is not MySQL

This is the most important point. D1 is SQLite — not MySQL, not PostgreSQL. Common differences you'll hit:

  • AUTO_INCREMENT is spelled AUTOINCREMENT in SQLite
  • No ENUM type — use TEXT with a check constraint
  • ALTER TABLE is very limited — you cannot rename columns, only add new ones
  • No stored procedures
  • No SHOW TABLES — use SELECT name FROM sqlite_master WHERE type='table'
  • JSON functions differ: json_extract(data, '$.key') instead of JSON_EXTRACT
  • No INFORMATION_SCHEMA

If you're using an ORM like Prisma or Drizzle, both support D1 but require the SQLite dialect — the schema and migrations will be different from a PostgreSQL project.

Not suitable for write-heavy workloads

5 million reads per day on the free tier is fine, but 100,000 writes per day is a tight limit. Apps with heavy writes — chat, logging, tracking — will hit the ceiling quickly.

Even on Workers Paid with 50 million writes per month (~1.67 million per day), D1 is not the right choice for write-intensive workloads.

SQLite has FTS5, but D1 doesn't fully expose it today. If you need search, you'll need to implement it yourself or use an external service.

Works in Workers and Pages — but not publicly accessible

D1 bindings work in both Cloudflare environments:

  • Cloudflare Workers: access via env.DB in the Worker script
  • Cloudflare Pages Functions: access via context.env.DB in function handlers under functions/
// Pages Function — functions/api/users.ts
export async function onRequestGet(context: EventContext<Env, string, unknown>) {
  const { results } = await context.env.DB.prepare(
    "SELECT id, name FROM users LIMIT 20"
  ).all();
  return Response.json(results);
}

D1 has no public connection string. There is no way to connect directly to D1 from outside the Cloudflare platform — no host, port, username, or password to configure like a standard MySQL or PostgreSQL database.

This means:

  • Cannot be used from a VPS, Docker container, or external server
  • Cannot be used from a NestJS, Express, or FastAPI backend running on your own server
  • Cannot connect with Prisma Studio or DBeaver directly to production D1
  • Inspecting data requires Wrangler CLI or the Cloudflare Dashboard

If you need to access D1 from outside for debugging or running scripts, the only options are a temporary Worker endpoint or Wrangler CLI:

# Query D1 from terminal via Wrangler (local)
wrangler d1 execute my-database --command "SELECT * FROM users LIMIT 10"

# Query remote (production database)
wrangler d1 execute my-database --remote --command "SELECT COUNT(*) FROM users"

Migrating out of D1 — why it's painful

This is what you need to know before committing to D1 for a long-running project.

Exporting data

# Export the entire database to a SQL file
wrangler d1 export my-database --output backup.sql

The output SQL file is SQLite-flavored. To import it into PostgreSQL or MySQL, you'll need to:

  1. Fix incompatible syntax (AUTOINCREMENT → AUTO_INCREMENT, type differences...)
  2. Write or find a conversion script
  3. Recreate the schema on the new database
  4. Import the data

There's no one-step tool that moves data from D1 to PlanetScale or Supabase. With a complex schema or many tables, this is a significant amount of work.

ORM schema is not portable

If you use Drizzle with the SQLite dialect for D1:

// Drizzle SQLite — only works with D1/SQLite
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  email: text("email").notNull().unique(),
});

When you move to PostgreSQL, you have to rewrite the entire schema using pgTable, swap types, redo migrations — nothing is reusable.

Who D1 is right for

Use D1 when:

  • Side project, internal tool, or personal app running on Workers
  • Read-heavy app with low write volume (blog, portfolio, content site)
  • Rapid prototyping where you don't want to deal with infrastructure
  • Already in the Cloudflare ecosystem and want to keep everything in one place

Don't use D1 when:

  • The project will eventually need to move to MySQL or PostgreSQL
  • Write-heavy workload: chat, logging, event tracking
  • Backend runs outside Workers (VPS, container, other serverless platforms)
  • You need full-text search, stored procedures, or advanced database features
  • The team is already on PostgreSQL and doesn't want to deal with SQLite quirks

Alternatives when you need more

Need Alternative
MySQL/PostgreSQL with free tier PlanetScale (MySQL), Supabase (PostgreSQL), Neon (PostgreSQL)
SQLite but not locked into Workers Turso (distributed SQLite with HTTP API)
Serverless PostgreSQL Neon, Supabase
Already on AWS RDS, Aurora Serverless