sellIntegration
alt_route

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.

5Storage options
$0R2 egress fees
~60sKV global sync
SQLD1 is relational
insights

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.

schemaFive stores behind one Worker

cache

SQL

files

state

accelerate

Browser

Worker

Workers KV

D1

R2

Durable Objects

Hyperdrive

Postgres / MySQL

kitchen

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.

account_tree

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.

schemaPick a store in four questions

Yes

No

Yes

Yes

No

No

Yes

No

What are you storing?

Large files / blobs?

Relational SQL queries?

Existing Postgres / MySQL?

Strong consistency + coordination?

Workers KV

D1

R2

Durable Objects

Hyperdrive

info

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.

target

The five options at a glance

One card per store, with the one-line rule for when to reach for it.

key

Workers KV — use when…

Tiny values read far more than written, and ~60s global sync is fine: caches, feature flags, config, sessions.

table

D1 — use when…

You need relational tables and SQL (JOIN, WHERE, ORDER BY) and want a Cloudflare-native database with no server to run.

inventory_2

R2 — use when…

You store large files — images, video, PDFs, backups — and want zero egress (download) fees.

hub

Durable Objects — use when…

One piece of state must be strongly consistent and coordinated: counters, chat rooms, game state, locks, live collaboration.

rocket_launch

Hyperdrive — use when…

You already have a Postgres/MySQL database and just want it fast from Workers — no migration, no rewrite.

construction

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.

  1. KV — get / put a value

    Look up by key; values are strings (or streams). Optional TTL auto-expires the key.

    js
    // 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 });
  2. D1 — prepared SQL query

    Write SQL with ? placeholders, bind() the values, then .all() for rows.

    js
    // wrangler.jsonc -> { "d1_databases": [{ "binding": "DB", "database_name": "app" }] }
    const { results } = await env.DB
      .prepare("SELECT * FROM orders WHERE city = ?")
      .bind("Taipei")
      .all();
  3. R2 — store / fetch an object

    put() a file by key, get() it back as an object whose .body is a stream.

    js
    // 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);
  4. 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.

    js
    // wrangler.jsonc -> durable_objects: { "name": "ROOM", "class_name": "Room" }
    const stub = env.ROOM.getByName("room-42");
    const count = await stub.increment(); // strongly consistent
  5. Hyperdrive — connection string

    Read env.HYPERDRIVE.connectionString and connect with a normal pg client; pooling and caching happen for you.

    js
    // 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");
school

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.

schedule

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.

verified

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.

table_rows

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.

data_object

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.

inventory_2

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.

hub

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.

schemaConsistency spectrum

Weaker consistency

Workers KV

D1

Durable Objects

Stronger consistency

tips_and_updates

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.
extension

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.

warning

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.