Which storage should I use? KV vs D1 vs R2 vs DO vs Hyperdrive
Five storage products, one Worker. Ask a few questions and the right one falls out.
What are we choosing between?
Cloudflare gives you five very different ways to store data, and they all sit behind your Worker. Picking the wrong one makes everything harder; picking the right one makes your code short and fast. This guide turns the choice into a few yes/no questions.
Here is the whole cast on one stage: the Worker is the brain, and each store plays one role. KV is the cache, D1 is the SQL table, R2 is the file shelf, Durable Objects is the single source of truth, and Hyperdrive is the fast lane to a database you already own.
Think of it like…
A kitchen. KV is the spice rack (grab-and-go, refilled occasionally), D1 is the recipe binder (structured, searchable), R2 is the walk-in freezer (big, bulky items), Durable Objects is the head chef who keeps one authoritative order list, and Hyperdrive is a delivery shortcut to the supplier you already use.
The decision tree
Start at the top and follow the answers. Each branch lands on exactly one product. You can always combine several later, but for any single piece of data this tree gives you a clear default.
Order matters
Ask about big files first (R2 is unmistakable), then relational needs (D1 vs Hyperdrive), then strong consistency (Durable Objects). If none of those fit, a small read-heavy value almost always means KV.
The five options at a glance
One card per store, with the one-line rule for when to reach for it.
Workers KV — use when…
Tiny values read far more than written, and ~60s global sync is fine: caches, feature flags, config, sessions.
D1 — use when…
You need relational tables and SQL (JOIN, WHERE, ORDER BY) and want a Cloudflare-native database with no server to run.
R2 — use when…
You store large files — images, video, PDFs, backups — and want zero egress (download) fees.
Durable Objects — use when…
One piece of state must be strongly consistent and coordinated: counters, chat rooms, game state, locks, live collaboration.
Hyperdrive — use when…
You already have a Postgres/MySQL database and just want it fast from Workers — no migration, no rewrite.
How each binding looks in code
A binding is a name you declare in wrangler.jsonc that shows up as env.SOMETHING inside your Worker. Here is the smallest real snippet for each store — notice how different the access patterns are.
KV — get / put a value
Look up by key; values are strings (or streams). Optional TTL auto-expires the key.
// wrangler.jsonc -> { "kv_namespaces": [{ "binding": "KV", "id": "..." }] } const cached = await env.KV.get("user:42"); await env.KV.put("user:42", JSON.stringify(user), { expirationTtl: 3600 });D1 — prepared SQL query
Write SQL with ? placeholders, bind() the values, then .all() for rows.
// wrangler.jsonc -> { "d1_databases": [{ "binding": "DB", "database_name": "app" }] } const { results } = await env.DB .prepare("SELECT * FROM orders WHERE city = ?") .bind("Taipei") .all();R2 — store / fetch an object
put() a file by key, get() it back as an object whose .body is a stream.
// wrangler.jsonc -> { "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "files" }] } await env.BUCKET.put("invoice.pdf", request.body); const object = await env.BUCKET.get("invoice.pdf"); return new Response(object.body);Durable Objects — call a stub
getByName() returns the ONE instance for that name; call its methods directly over RPC. Every caller hits the same object.
// wrangler.jsonc -> durable_objects: { "name": "ROOM", "class_name": "Room" } const stub = env.ROOM.getByName("room-42"); const count = await stub.increment(); // strongly consistentHyperdrive — connection string
Read env.HYPERDRIVE.connectionString and connect with a normal pg client; pooling and caching happen for you.
// wrangler.jsonc -> { "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "..." }] } import { Client } from "pg"; const client = new Client({ connectionString: env.HYPERDRIVE.connectionString }); await client.connect(); const { rows } = await client.query("SELECT * FROM products LIMIT 10");
The words behind the choice
Three ideas decide most of these picks: consistency, relational vs key-value, and object storage. Get these and the tree becomes obvious.
Eventual consistency
After a write, other regions may take up to ~60s to show the new value — they catch up 'eventually'. KV works this way: great for caches, wrong for bank balances.
Strong consistency
Every reader always sees the latest write, immediately. A Durable Object gives this because exactly one instance owns the data and processes requests in order.
Relational (SQL)
Data lives in tables with columns and relationships, queried with SQL — JOIN links tables, WHERE filters, ORDER BY sorts. D1 (and your own DB via Hyperdrive) are relational.
Key-value
No tables, no SQL — just a name (key) pointing to a value. Fast and simple, but you can only look things up by the exact key. KV is the pure key-value store.
Object storage
Built for large, opaque files ('objects') like images and video, addressed by a key. R2 is object storage with no egress fee for downloading those files.
Coordination
When many users act on the same shared thing — same counter, same chat room — someone must serialize them so updates don't clash. That single owner is exactly what Durable Objects provides.
Cheat-sheet, mixing & gotchas
Use X when…
- Use KV when: tiny values are read far more than written and ~60s global sync is fine (cache, config, sessions).
- Use D1 when: you need relational tables and SQL queries (JOIN/WHERE/ORDER BY) in a Cloudflare-native database.
- Use R2 when: you store big files — images, video, PDFs, backups — and want zero egress fees.
- Use Durable Objects when: one shared piece of state must be strongly consistent and coordinated (counters, rooms, locks).
- Use Hyperdrive when: you already run Postgres/MySQL elsewhere and just want it fast from Workers — no migration.
You can mix them
Real apps combine several: store the upload in R2, keep its metadata in D1, cache the hot lookups in KV, and let a Durable Object guard the one counter that must never double-count.
Common mis-picks
Don't store large files in KV (25 MiB cap and it's not built for blobs) and don't use KV for must-be-fresh-everywhere data. Don't reach for Durable Objects just to hold static data a table would handle. Don't pick Hyperdrive if you have no existing database — start with D1.
- KV: read-heavy, eventually consistent, key-value only, up to 25 MiB per value.
- D1: SQLite-based relational SQL, read replicas, great default for app data.
- R2: S3-compatible object storage, no egress fees, ideal for media and backups.
- Durable Objects: one instance per name, strongly consistent, has its own embedded SQLite.
- Hyperdrive: pooling + caching in front of your existing Postgres/MySQL — not a new database.
Related products
menu_bookOfficial docsopen_in_new