diff --git a/src/content/docs/d1/best-practices/d1-complete-guide.mdx b/src/content/docs/d1/best-practices/d1-complete-guide.mdx new file mode 100644 index 00000000000..4e35f3826ab --- /dev/null +++ b/src/content/docs/d1/best-practices/d1-complete-guide.mdx @@ -0,0 +1,746 @@ +# The Complete Guide to Cloudflare D1 + +> A practical, opinionated guide for developers new to Cloudflare, Workers users ready for seamless storage, and AI agents building apps for hobbyists. + +--- + +## Quick Navigation + +1. [Is D1 Right for You?](#is-d1-right-for-you) +2. [What Is D1?](#what-is-d1) +3. [How D1 Works](#how-d1-works) +4. [Getting Started in 5 Minutes](#getting-started-in-5-minutes) +5. [Optimization & Best Practices](#optimization--best-practices) +6. [Location Hints & Global Placement](#location-hints--global-placement) +7. [Read Replication & Sessions API](#read-replication--sessions-api) +8. [Foreign Keys & Schema Migrations](#foreign-keys--schema-migrations) +9. [Troubleshooting Common Issues](#troubleshooting-common-issues) +10. [Quick Reference Cheat Sheet](#quick-reference-cheat-sheet) + +--- + +## Is D1 Right for You? + +Before you write a single query, ask two questions: + +1. **Is my workload read-heavy or write-heavy?** +2. **Do I need strict global consistency on every read?** + +### The Decision Matrix + +| Your Use Case | Good Fit? | Why | +|---|---|---| +| User profiles & configuration data | ✅ Excellent | Small rows, frequent reads, infrequent writes | +| Blog posts, CMS content, documentation | ✅ Excellent | Read-heavy, globally distributed readers | +| URL shorteners | ✅ Excellent | Lightning-fast lookups, simple schema | +| E-commerce product catalogs | ✅ Excellent | Read-heavy catalog with occasional inventory updates | +| IoT device states | ✅ Good | Edge-local writes, periodic sync | +| Multiplayer game leaderboards | ⚠️ Okay | Consider if write volume is moderate | +| High-frequency trading / real-time bidding | ❌ Poor | Too write-heavy, needs strict serializability | +| Massive write-heavy analytics | ❌ Poor | D1 is single-threaded per database | +| Multi-GB datasets with complex joins | ❌ Poor | 10 GB limit per DB, memory constraints on large scans | +| Strict global serializability required | ❌ Poor | D1 uses async replication | +| Data warehouse / OLAP | ❌ Poor | Not designed for analytical workloads | + +### The Rule of Thumb + +If you need a SQL database for a Cloudflare Worker or Pages project, and you don't need exotic PostgreSQL features like `JSONB` operators, `SERIAL` types, or stored procedures, **D1 is the path of least resistance**. + +--- + +## What Is D1? + +D1 is Cloudflare's managed, serverless SQL database. It uses SQLite semantics, runs on Cloudflare's global network (300+ cities), and connects directly to Workers and Pages without connection strings. + +### Three Concepts Every Beginner Must Know + +#### 1. SQLite at the Core + +D1 is built on **SQLite**, the most widely deployed database engine on earth. It lives in your phone, your browser, and likely your car. + +Unlike PostgreSQL or MySQL, SQLite is not a client-server database. It is a library that reads and writes a single file. D1 takes that file and makes it serverless and globally distributed. + +#### 2. Serverless Means Zero Infrastructure + +You do not rent a database server by the hour. You do not pick instance sizes. You write a query, Cloudflare runs it, and you pay for the milliseconds it took (or nothing, within free-tier limits). When nobody uses your app, the database effectively costs zero. + +#### 3. Edge Location Means Speed + +Traditional cloud databases live in one to three regions (for example, `us-east-1`). D1 places your data at the edge. Your application code runs in Tokyo, and your database query runs in Tokyo too. No round-trip to Virginia. + +--- + +## How D1 Works + +### Storage & Replication + +Your data lives in a single SQLite file. Cloudflare replicates this file across its network. + +![D1 read replication concept](https://developers.cloudflare.com/images/d1/d1-read-replication-concept.png) + +*Image: Cloudflare D1 read replication architecture. Read replicas serve read traffic locally while writes flow to the primary.* + +### The Consistency Model + +D1 uses **asynchronous replication**. This is the most important concept in this guide. + +- **Read your writes:** A user in London writes data, then reads it back in London. It is usually there immediately. +- **Global consistency:** A user in London writes data, and a user in Sydney reads it 10 milliseconds later. It might not be there yet. + +That tiny delay is called **replica lag**. For most web applications, this is imperceptible. For financial ledgers, it might be a dealbreaker. + +### Where D1 Runs + +D1 databases are backed by **Durable Objects** pinned to specific geographic regions. You can see exactly where Cloudflare's data centers live by visiting [where.durableobjects.live](https://where.durableobjects.live/colo/HYD). + +When you create a database without a location hint, Cloudflare places it near the request that created it. That might be your CI/CD pipeline in Oregon, even though your users are in Frankfurt. We will fix that later. + +### What a Single Database Can Handle + +Each D1 database is inherently **single-threaded**. It processes queries one at a time. + +| Average Query Duration | Approximate Max Throughput | +|---|---| +| 1 ms | ~1,000 queries/second | +| 10 ms | ~100 queries/second | +| 100 ms | ~10 queries/second | + +If a database receives too many concurrent requests, it queues them. If the queue fills up, it returns an **overloaded** error. + +**Key insight:** D1 is designed for horizontal scale-out across many smaller databases (for example, one per user, per tenant, or per project), not for vertical scale-up of one massive database. + +--- + +## Getting Started in 5 Minutes + +### Prerequisites + +You need [Node.js](https://nodejs.org/) installed. Then install Wrangler, Cloudflare's CLI: + +```bash +npm install -g wrangler +npx wrangler login +``` + +### Step 1: Create a Database + +```bash +npx wrangler d1 create my-first-db +``` + +Wrangler will return a configuration block. Save it. + +### Step 2: Create a Schema + +Create a file named `schema.sql`: + +```sql +DROP TABLE IF EXISTS links; +CREATE TABLE links ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + slug TEXT UNIQUE NOT NULL, + url TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +### Step 3: Run the Schema + +```bash +npx wrangler d1 execute my-first-db --file=./schema.sql +``` + +### Step 4: Bind the Database to a Worker + +Create or edit `wrangler.toml`: + +```toml +name = "my-d1-app" +main = "src/index.js" +compatibility_date = "2024-01-01" + +[[d1_databases]] +binding = "DB" # how you refer to it in code +database_name = "my-first-db" +database_id = "your-uuid-here" +``` + +### Step 5: Write Worker Code + +Create `src/index.js`: + +```javascript +export default { + async fetch(request, env) { + const url = new URL(request.url); + const slug = url.pathname.slice(1); + + if (request.method === "POST" && slug === "create") { + const body = await request.json(); + await env.DB.prepare( + "INSERT INTO links (slug, url) VALUES (?, ?)" + ).bind(body.slug, body.url).run(); + return new Response("Created", { status: 201 }); + } + + const result = await env.DB.prepare( + "SELECT url FROM links WHERE slug = ?" + ).bind(slug).first(); + + if (result) { + return Response.redirect(result.url, 302); + } + + return new Response("Not found", { status: 404 }); + } +}; +``` + +Notice: `env.DB` is just there. No connection strings. No TLS certificates. No connection pools. + +### Step 6: Deploy + +```bash +npx wrangler deploy +``` + +That is it. You have a globally distributed database-backed application in roughly 20 lines of code. + +--- + +## Optimization & Best Practices + +The following ten rules are the difference between a D1 app that flies and one that crawls. + +### 1. Index Every Column Used in Predicates, Joins, and Sort Keys + +D1 is single-threaded per database. A table scan taking 100 milliseconds limits you to roughly 10 queries per second. A point lookup on an indexed column takes less than 1 millisecond. + +```sql +-- Good: indexed lookup +SELECT * FROM users WHERE email = ?; + +-- Bad: full table scan +SELECT * FROM users WHERE email = ?; -- without an index on email +``` + +Create indexes during schema design: + +```sql +CREATE INDEX idx_users_email ON users(email); +``` + +After creating or dropping an index, always run: + +```sql +PRAGMA optimize; +``` + +This collects statistics so the query planner makes smart choices. + +**💡 Tip:** You can inspect slow queries in production using `wrangler d1 insights`, the GraphQL API, or the D1 dashboard UI. + +**⚠️ Warning:** Every indexed column adds at least one extra row written per `INSERT` or `UPDATE`. This is always worth it for read-heavy workloads, but be conscious of write amplification on write-heavy tables. + +**⚠️ Warning:** Indexes are hard to add after-the-fact on multi-gigabyte databases. Add them upfront whenever possible. + +### 2. Batch Related Queries with `db.batch()` + +All queries in a `batch()` call share a single transaction and a single round-trip. Calling `.run()` in a loop issues one round-trip per query. This is the single most impactful latency win for write-heavy operations. + +```javascript +// Bad: N round-trips +for (const user of users) { + await env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind(user.name).run(); +} + +// Good: 1 round-trip +const statements = users.map(u => + env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind(u.name) +); +await env.DB.batch(statements); +``` + +### 3. Implement Application-Level Retry with Exponential Backoff + +D1 automatically retries read-only queries (`SELECT`, `EXPLAIN`, `WITH`) up to two times on transient errors. + +**Write queries are never auto-retried.** Your application must implement its own retry for idempotent write operations. + +Mirror this pattern: + +- Baseline delay: ~50 ms +- Cap: ~2 seconds +- Strategy: full jitter +- Max attempts: 3 + +Only retry on `D1_ERROR` codes marked as retryable. Here is a robust TypeScript example: + +```typescript +function shouldRetry(err: unknown, nextAttempt: number): boolean { + const errMsg = String(err); + const isRetryable = + errMsg.includes("Network connection lost") || + errMsg.includes("storage caused object to be reset") || + errMsg.includes("reset because its code was updated"); + return nextAttempt <= 3 && isRetryable; +} + +async function queryWithRetry(d1: D1Database, sql: string, params: unknown[]) { + let attempt = 0; + while (true) { + try { + return await d1.prepare(sql).bind(...params).run(); + } catch (err) { + attempt++; + if (!shouldRetry(err, attempt)) throw err; + const delay = Math.min(50 * Math.pow(2, attempt), 2000); + const jitter = Math.random() * delay; + await new Promise(r => setTimeout(r, jitter)); + } + } +} +``` + +See the [error list](https://developers.cloudflare.com/d1/observability/debug-d1/#error-list) for exactly which errors are retryable. + +### 4. Chunk Bulk Mutations + +Never modify hundreds of thousands of rows in one query. A single `UPDATE` or `DELETE` over a massive table can exceed CPU or memory limits, causing a Durable Object reset. The in-flight operation is lost. + +Process in chunks of roughly 10,000 rows: + +```javascript +const CHUNK_SIZE = 10000; +let offset = 0; +while (true) { + const result = await env.DB.prepare( + `DELETE FROM old_logs WHERE id IN ( + SELECT id FROM old_logs WHERE created_at < ? + LIMIT ? + )` + ).bind(cutoffDate, CHUNK_SIZE).run(); + + if (result.meta.changes < CHUNK_SIZE) break; +} +``` + +The same applies to large imports. Partition data into files of tens of megabytes rather than hundreds. + +### 5. Always Use Parameterized Queries with `.bind()` + +Never interpolate user data into SQL strings. + +```javascript +// Never do this +const stmt = env.DB.prepare(`SELECT * FROM users WHERE email = '${email}'`); + +// Always do this +const stmt = env.DB.prepare("SELECT * FROM users WHERE email = ?").bind(email); +``` + +Beyond SQL injection protection, bound parameters are stripped from D1 query metrics (`d1 insights`). This lets you aggregate analytics across all executions of the same query template instead of fragmenting them by every unique value. + +### 6. Set a Location Hint That Matches Your Primary Query Region + +D1 databases are backed by Durable Objects pinned to a region. Without a location hint, Cloudflare picks a region close to the request creating the database. If your CI pipeline runs in Oregon but your users are in Europe, your database lives in Oregon. + +Cross-region requests add significant round-trip latency to every query. + +Set a location hint at creation time: + +```bash +npx wrangler d1 create my-db --location=weur +``` + +Available hints: + +| Hint | Region | +|---|---| +| `wnam` | Western North America | +| `enam` | Eastern North America | +| `weur` | Western Europe | +| `eeur` | Eastern Europe | +| `apac` | Asia-Pacific | +| `oc` | Oceania | + +**💡 Tip:** For a global, high-traffic property like a public blog, default to `enam` or `weur`. These regions are closest to the majority of internet users. + +**⚠️ Warning:** Location hints are not currently supported for South America (`sam`), Africa (`afr`), or the Middle East (`me`). D1 databases do not run in these locations. The hint will fall back to the nearest available region. + +### 7. Foreign Keys Are On by Default + +Unlike vanilla SQLite, D1 enforces foreign keys in every transaction. If you are migrating a schema designed with `PRAGMA foreign_keys = off`, existing violations will surface immediately. + +For migrations and imports where you need to temporarily suspend enforcement, use: + +```sql +PRAGMA defer_foreign_keys = on; +``` + +Do **not** use `PRAGMA foreign_keys = off`. It is silently ignored in D1 because every query runs inside an implicit transaction. + +```sql +-- In your migration script +PRAGMA defer_foreign_keys = on; + +-- Run ALTER TABLE, DROP COLUMN, etc. + +-- Enforcement resumes at transaction end +PRAGMA defer_foreign_keys = off; +``` + +If outstanding violations remain when the transaction ends, you will receive a `FOREIGN KEY constraint failed` error. + +### 8. Capture a Time Travel Bookmark Before Destructive Migrations + +[Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/) is always on and costs nothing. It lets you restore a database to any minute in the last 30 days (7 days on the Free plan). + +Before any destructive migration, capture a bookmark: + +```bash +npx wrangler d1 time-travel info my-db +``` + +Store that bookmark somewhere safe. If the migration goes wrong, restore in seconds: + +```bash +npx wrangler d1 time-travel restore my-db --bookmark=YOUR_BOOKMARK +``` + +**⚠️ Warning:** Restoring is a destructive, in-place overwrite. In-flight queries are cancelled. Bookmarks older than 30 days expire and cannot be used. + +### 9. Use `db.withSession()` When Read Replication Is Enabled + +Read replicas reduce latency for read-heavy apps with global users. Even if you don't enable read replication today, design your code to use the **Sessions API** from the start. This lets you toggle replication on later without changing Worker code. + +The `withSession()` function returns an object with the same interface as the top-level database object. + +```javascript +// Start a session +const session = env.DB.withSession("first-unconstrained"); + +// Use it exactly like env.DB +const result = await session.prepare("SELECT * FROM products WHERE id = ?").bind(id).all(); + +// Pass the bookmark to the next request for sequential consistency +const bookmark = session.getBookmark(); +``` + +Session types: + +| Parameter | Behavior | +|---|---| +| `first-unconstrained` (default) | First query routes to any replica for minimum latency. | +| `first-primary` | First query routes to the primary for the latest data. | +| `` | Starts from a specific previous state, guaranteeing monotonic reads. | + +![Sequential consistency with Sessions API](https://developers.cloudflare.com/images/d1/consistency-with-sessions-api.png) + +*Image: How the Sessions API uses bookmarks to ensure you never read stale data after your own writes.* + +**💡 Tip:** Return the bookmark to your client (for example, in an HTTP header) so subsequent requests from the same user session maintain consistency. + +### 10. Respect the Resource Limits of a Single Database + +A single D1 database has fixed resources. A common gotcha with large databases (5 GB+) is that aggregation queries scanning the entire table into memory often fail due to CPU or memory limits. + +Key limits to memorize: + +| Limit | Value | +|---|---| +| Max database size | 10 GB (Paid) / 500 MB (Free) | +| Max rows read per query | Effectively limited by CPU/memory | +| Max query duration | 30 seconds | +| Max SQL statement length | 100 KB | +| Max bound parameters | 100 | +| Max string/BLOB/row size | 2 MB | + +If you outgrow one database, shard your data horizontally across multiple databases. D1 allows tens of thousands of databases per account at no extra management cost. + +--- + +## Location Hints & Global Placement + +### When to Use a Location Hint + +Provide a hint when: +- Your team is distributed, but your users are concentrated in one region. +- You use Infrastructure-as-Code to create databases programmatically (the creator's location may not match the users' location). +- You are building per-tenant databases and each tenant serves a specific geography. + +### Jurisdictions + +If you need to comply with data locality regulations like GDPR or FedRAMP, use a jurisdiction constraint at creation time. This overrides location hints. + +```bash +npx wrangler d1 create eu-db --jurisdiction=eu +``` + +Supported jurisdictions: `eu`, `fedramp`. + +**⚠️ Warning:** Jurisdictions can only be set on database creation and cannot be changed later. + +--- + +## Read Replication & Sessions API + +### Why Read Replication Matters + +Without read replication, every query, read or write, travels to the primary database instance. A user in Sydney hitting a primary in Virginia pays a 200 ms round-trip on every request. + +With read replication enabled, D1 creates read-only copies of your database in every supported region. The Sydney user queries the Sydney replica in single-digit milliseconds. + +![Without Sessions API, replicas can be inconsistent](https://developers.cloudflare.com/images/d1/consistency-without-sessions-api.png) + +*Image: Without the Sessions API, a read replica might serve stale data immediately after a write.* + +### Enabling Read Replication + +In the Cloudflare dashboard, go to **D1** > select your database > **Settings** > **Enable Read Replication**. + +Or use the REST API: + +```bash +curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/d1/database/{database_id}" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"read_replication": {"mode": "auto"}}' +``` + +Read replication is built into D1 pricing. You do not pay extra for replica storage or compute. Billing is based on `rows_read` and `rows_written`, regardless of which instance serves the query. + +### Observability + +Inspect where a query was served using the `meta` object: + +```javascript +const result = await env.DB.withSession().prepare("SELECT * FROM users").run(); +console.log({ + region: result.meta.served_by_region, + primary: result.meta.served_by_primary +}); +``` + +| Field | Meaning | +|---|---| +| `served_by_region` | The region that processed the query | +| `served_by_primary` | `true` if the primary served it, `false` if a replica did | + +--- + +## Foreign Keys & Schema Migrations + +### Defining Relationships + +D1 supports standard SQLite foreign key syntax: + +```sql +CREATE TABLE users ( + user_id INTEGER PRIMARY KEY, + email_address TEXT +); + +CREATE TABLE orders ( + order_id INTEGER PRIMARY KEY, + user_who_ordered INTEGER, + FOREIGN KEY(user_who_ordered) REFERENCES users(user_id) ON DELETE RESTRICT +); +``` + +Available actions: `CASCADE`, `RESTRICT`, `SET DEFAULT`, `SET NULL`, `NO ACTION`. + +**⚠️ Warning:** `CASCADE` deletes child rows automatically. This can have unintended side effects. Double-check your application logic before using it. + +### Safe Migration Checklist + +1. Capture a Time Travel bookmark: `wrangler d1 time-travel info my-db` +2. Test migrations in a local or staging database first: `wrangler dev --local` +3. Use `PRAGMA defer_foreign_keys = on` if the migration temporarily violates constraints +4. Run `PRAGMA optimize` after schema changes +5. Verify with `EXPLAIN QUERY PLAN` that your indexes are still hit + +--- + +## Troubleshooting Common Issues + +### My Queries Are Slow + +**Symptom:** Requests take hundreds of milliseconds or time out. + +**Diagnosis:** +- Run `EXPLAIN QUERY PLAN SELECT ...` to check for table scans. +- Inspect `wrangler d1 insights` for slow queries in production. +- Check if you are querying from a region far from your database's primary location. + +**Fix:** +- Add indexes on columns in `WHERE`, `JOIN`, and `ORDER BY` clauses. +- Run `PRAGMA optimize` after adding indexes. +- Set a location hint closer to your users. +- Enable read replication and use the Sessions API. + +### I Hit a `D1 DB is overloaded` Error + +**Symptom:** `D1 DB is overloaded. Requests queued for too long.` + +**Diagnosis:** Your database is receiving more concurrent requests than it can process, or individual queries are too slow. + +**Fix:** +- Optimize query performance (add indexes, reduce rows scanned). +- Batch writes to reduce round-trips. +- Shard data across multiple databases if you have tens of thousands of users. +- Implement client-side rate limiting or backpressure. + +### I Hit a `D1 DB exceeded its CPU time limit` Error + +**Symptom:** `D1 DB exceeded its CPU time limit and was reset.` + +**Diagnosis:** A query scanned too many rows or joined too many tables. + +**Fix:** +- Break large `UPDATE` or `DELETE` statements into chunks of ~10,000 rows. +- Add indexes so the query planner avoids full table scans. +- Avoid `SELECT *` on massive tables without `LIMIT` clauses. + +### My Write Failed with a Transient Error + +**Symptom:** `Network connection lost` or `Internal error in D1 DB storage caused object to be reset.` + +**Diagnosis:** D1 automatically retries read-only queries, but never retries writes. + +**Fix:** +- Implement application-level retry with exponential backoff and jitter. +- Only retry idempotent operations. +- Verify you are checking for retryable error strings as shown in [Best Practice #3](#3-implement-application-level-retry-with-exponential-backoff). + +### My Migration Fails with `FOREIGN KEY constraint failed` + +**Symptom:** A schema migration or import fails despite working in vanilla SQLite. + +**Diagnosis:** D1 enforces foreign keys by default. Your migration may temporarily create orphan rows. + +**Fix:** +- Use `PRAGMA defer_foreign_keys = on` at the start of the migration transaction. +- Do **not** use `PRAGMA foreign_keys = off`; it is silently ignored. +- Resolve all violations before the transaction commits. + +### I Can't Connect from My Local GUI (TablePlus, DBeaver, etc.) + +**Symptom:** You try to connect via a standard database client and it fails. + +**Diagnosis:** D1 does not expose a TCP socket or standard connection string. + +**Fix:** +- Use Wrangler for ad-hoc queries: `npx wrangler d1 execute my-db --command="SELECT ..."` +- Use the Worker binding API for application queries. +- For local development, `wrangler dev` spins up a local SQLite file you can inspect. + +### My Read Replica Data Is Stale + +**Symptom:** A user submits a form, then reloads the page and doesn't see their update. + +**Diagnosis:** Without the Sessions API, read replicas may serve data from before the write replicated. + +**Fix:** +- Use `db.withSession()` and pass bookmarks between requests. +- Start critical sessions with `first-primary` if the latest data is required immediately. + +--- + +## Quick Reference Cheat Sheet + +### For AI Agents and Hobbyists + +Use this section as a prompt-friendly reference when generating D1-powered applications. + +#### Schema Template + +```sql +-- Always include IF NOT EXISTS +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Index every queried column +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + +-- Optimize after schema changes +PRAGMA optimize; +``` + +#### Worker Template + +```javascript +export default { + async fetch(request, env) { + const session = env.DB.withSession("first-unconstrained"); + + try { + const result = await session + .prepare("SELECT * FROM users WHERE email = ?") + .bind(email) + .all(); + + const response = Response.json(result.results); + response.headers.set("x-d1-bookmark", session.getBookmark() ?? ""); + return response; + } catch (e) { + return Response.json({ error: e.message }, { status: 500 }); + } + } +}; +``` + +#### Retry Helper + +```typescript +async function runWithRetry(fn: () => Promise): Promise { + let attempt = 0; + while (true) { + try { + return await fn(); + } catch (err) { + const msg = String(err); + const retryable = + msg.includes("Network connection lost") || + msg.includes("storage caused object to be reset") || + msg.includes("reset because its code was updated"); + attempt++; + if (!retryable || attempt > 3) throw err; + await new Promise(r => setTimeout(r, Math.random() * Math.min(50 * 2 ** attempt, 2000))); + } + } +} +``` + +#### Checklist Before Shipping + +- [ ] Indexes exist for all `WHERE`, `JOIN`, and `ORDER BY` columns +- [ ] `PRAGMA optimize` run after schema changes +- [ ] Parameterized queries with `.bind()` used everywhere +- [ ] `db.batch()` used for bulk operations +- [ ] Application-level retry implemented for writes +- [ ] Location hint set at database creation +- [ ] Time Travel bookmark captured before destructive migrations +- [ ] `withSession()` used if read replication is or will be enabled +- [ ] Chunk size for bulk mutations capped at ~10,000 rows +- [ ] Query duration verified under 100 ms for hot paths + +--- + +## Additional Resources + +- [Cloudflare D1 Documentation](https://developers.cloudflare.com/d1/) +- [D1 Best Practices](https://developers.cloudflare.com/d1/best-practices/) +- [D1 Limits](https://developers.cloudflare.com/d1/platform/limits/) +- [D1 Pricing](https://developers.cloudflare.com/d1/platform/pricing/) +- [Read Replication Deep Dive](https://blog.cloudflare.com/d1-read-replication-beta/) +- [Building D1: A Global Database](https://blog.cloudflare.com/building-d1-a-global-database/) +- [Durable Objects Location Tracker](https://where.durableobjects.live/colo/HYD) +- [Debug D1 & Error List](https://developers.cloudflare.com/d1/observability/debug-d1/) +- [Time Travel & Backups](https://developers.cloudflare.com/d1/reference/time-travel/) +- [Foreign Keys in D1](https://developers.cloudflare.com/d1/sql-api/foreign-keys/) +- [Community Projects](https://developers.cloudflare.com/d1/reference/community-projects/)