Skip to content

Database resolution ​

Product.find(id) works with no setup, because a Model knows its D1 binding. But a Worker isn’t a long-running server, and that shapes how a query finds its database.

One isolate, many requests ​

A Worker runs in an isolate that serves many requests at the same time, and a Model class is shared by all of them. So anything stored on a class is shared by every request in flight. That rules out storing a connection on a class: some of what a connection carries belongs to one request only.

  • A D1 Session keeps one user’s reads at least as new as their own writes. Shared, one user’s Session would serve another’s queries.
  • An onQuery listener might tag each query with its request’s ID. Shared, it would mix requests.

Rails doesn’t keep its connection on the class either: each thread checks one out from a pool, and gives it back afterwards. The JavaScript equivalent of “per request, without passing it around” is AsyncLocalStorage, which follows a request through every await.

Three ways to find a database ​

A query finds its database in one of three ways, in this order:

  1. The request’s database. Inside withDatabase(connect(env.DB, { session, onQuery }), fn), everything fn awaits uses that database, kept in the request’s AsyncLocalStorage, never on a class. It’s matched to Models by binding, so a request can open several.
  2. The Model’s binding. Outside withDatabase, a Model uses the D1 binding its base class names (static binding = "DB"), read from cloudflare:workers. The binding is the same object for every request, and the database made for it holds nothing per request (no Session, no listener), so sharing it is safe. This is what makes Product.find(id) work with no setup.
  3. An explicit database. connect(env.DB).model(Product) always uses that database, whatever withDatabase says. It suits code that passes its database around, or uses two at once.

Most apps only ever use the second. The first exists for what belongs to a request: Sessions, per-request logging, and tests, which run outside a Worker and have no binding to find.

Which do I need? ​

Your situationWhat to do
A Worker with one D1 databaseNothing: User.find(id) uses its binding
D1 read replication, reading your own writesA Session per request
Logging queries, or Server-TimingonQuery, app-wide or per request
Vitest tests, seed or migration scriptswithDatabase(connect(binding), …), or db.model(...)
Several D1 databasesOne base class per binding
A compatibility date before 2026-08-04, using withDatabaseAdd nodejs_compat to your Wrangler config’s compatibility_flags

What follows from it ​

  • Records remember. A record keeps the database it was loaded through, or first saved through, and so do the records saved with it (autosaved records, a belongsTo target), so post.save() and post.user() use it later, even after withDatabase has returned.
  • One query or write, one database. merge, or, scopes, batches, and saves each run on one database. What counts is the database each side resolves to when it runs, so inside withDatabase a loaded record’s user.posts() merges with Post.published(). Two databases are refused with IncompatibleRelation, before anything is written.
  • Errors say where the database should come from. A query outside a Worker with no withDatabase, a Model with no binding, or a query at a module’s top level (where Workers allow no I/O) each throws NoDatabase, saying what to do.

For the options themselves, see Connections.