---
url: https://docs.forestfuture.dev/d1-record/guide/d1.md
description: >-
  What's different on D1: no interactive transactions, keys generated in code,
  read replicas, uniqueness races, bound-parameter limits, foreign keys.
---

# D1’s limits

d1-record follows Rails wherever D1 allows. These are the places D1 works differently, worth knowing before you design around them.

## No interactive transactions

D1 can’t hold a transaction open while your Worker awaits, so there’s no `transaction(async (tx) => …)`. You can’t read, decide, then write atomically.

What you can make atomic is a set of writes prepared in advance: [`batch([...])`](./batches#batching-writes) sends statements and records together, and D1 writes all of them or none. d1-record uses the same batches itself:

* A destroy’s [`dependent` strategies](./associations#destroying-dependent-records) are written in one batch with the owner’s DELETE.
* A record [autosaved](./associations#autosaving) with its owner joins the owner’s batch when the owner’s key is known in advance.

## Generate keys in code

Rows in a batch can only point at each other through keys that exist before it’s sent. Generated `TEXT` keys are made in code by [`generateId`](./models#generating-ids), so records have their key from `build()`. Then:

* A graph of new records built through associations saves in one atomic batch.
* A batch can insert rows that point at each other.

When D1 generates the key (`INTEGER PRIMARY KEY`), it doesn’t exist until the row is written. An owner is written first, then the records built through it in the next batch, a level at a time, and the batches aren’t atomic together. See [Autosaving](./associations#autosaving).

## Read replicas

With read replication, a read can come from a replica that’s slightly behind. Open each request’s database with a [Session](./read-your-own-writes), so a request reads its own writes, and pass its bookmark to the next request, so a user never sees data older than their last write.

## Uniqueness is checked twice

The `uniqueness` validation asks D1 before saving, which gives a friendly error, but two requests can both pass it at once. Only a UNIQUE index guarantees uniqueness. Declare both, and handle [`RecordNotUnique`](./errors#constraint-errors) where a race matters.

`caseSensitive: false` compares with SQLite’s `lower()`, which only lowercases ASCII letters, so `"Ä"` and `"ä"` are still different.

## Bound parameters

D1 allows 100 bound values in one statement. d1-record binds lists as one JSON value (`json_each`), so `where({ id: [...] })`, `find([...])`, preloads, and `insertAll` work with any number of values. A single value can be up to 2 MB.

## Foreign keys are enforced

D1 enforces the foreign keys a table declares with `REFERENCES`. Destroying a record whose rows still point at it throws `InvalidForeignKey`, unless the association has a [`dependent` strategy](./associations#destroying-dependent-records) that removes or updates those rows first. A foreign key that isn’t declared in the table isn’t checked at all.

## Nothing rolls back after a write

Once D1 has accepted a write, it’s done. An error thrown by an after-callback reaches the caller, but the record stays saved.
