# Transactions (/docs/orm/fundamentals/transactions)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL.

Run several writes so they all succeed or all fail together with db.transaction().

Location: ORM > Fundamentals > Transactions

Run several writes as one unit with `db.transaction(...)`: they all commit together, or they all roll back.

Prisma ORM 8 runs on PostgreSQL, SQLite, and MongoDB. There is no MySQL support. The examples on this page are PostgreSQL; `db.transaction(...)` works the same way on SQLite, and MongoDB is covered in [Transactions on MongoDB](#transactions-on-mongodb).

## Run writes in a transaction [#run-writes-in-a-transaction]

Use a transaction whenever one business operation spans more than one write: creating a user with their first records, moving a value between two rows, or deleting a parent after its children. Pass a callback to `db.transaction(...)`. Inside it, query through `tx` instead of `db`. `tx.orm` is the model API, what `prisma.user` was in Prisma ORM 7; `tx.sql` is the SQL query builder. Both work the same as `db.orm` and `db.sql`. Every query you make on `tx` runs inside the transaction, and `db.transaction(...)` returns whatever your callback returns:

```typescript
import { db } from "./prisma/db";
const result = await db.transaction(async (tx) => {
  const user = await tx.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" });
  const post = await tx.orm.public.Post.create({ title: "Hello", published: false, authorId: user.id });
  return { userId: user.id, postId: post.id }; // both records exist once this returns
});
```

`db` is the client you create once in `src/prisma/db.ts`, and `prisma orm init` writes that file for you. It writes the file once; it is your code, so edit it freely. Under `tx.orm.public`, the property name is the model name, so `User` and `Post` are spelled exactly as you spelled them in your contract.

`public` is the PostgreSQL schema, the namespace your tables live in. It is `public` unless you set one. `create(...)` takes the fields of the record directly, with no `data:` wrapper. Pass a field if it is required and has no default. Everything else is optional, and [Write data](https://www.prisma.io/docs/orm/fundamentals/writing-data) has the full rules. You don't need a transaction for a single write; every mutation is already atomic on its own.

### Read inside a transaction [#read-inside-a-transaction]

Reads work the same way as writes. Query through `tx.orm` and you see the rows the transaction has already written. `.first()` returns `null` when nothing matches.

```typescript
await db.transaction(async (tx) => {
  await tx.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" });
  const jane = await tx.orm.public.User.where({ email: "jane@prisma.io" }).first();
  // jane is the record created a line earlier, before the transaction commits
});
```

### Pass tx to your helper functions [#pass-tx-to-your-helper-functions]

A helper that runs inside the caller's transaction takes `tx` as a parameter:

```typescript
import { db, type Tx } from "./prisma/db";
async function createWelcomePost(tx: Tx, authorId: string) {
  return tx.orm.public.Post.create({ title: "Welcome", published: false, authorId });
}
await db.transaction(async (tx) => {
  const user = await tx.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" });
  await createWelcomePost(tx, user.id);
});
```

Prisma ORM does not export a type for `tx`. Add this line to `src/prisma/db.ts`: `export type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];`, then import `Tx` wherever a helper needs it.
Do not open a second transaction inside the helper. Calling `db.transaction(...)` again inside a callback does not nest: the inner call is a separate transaction that commits or rolls back on its own, and the outer one cannot roll it back. Pass `tx` down instead.

### Options and isolation level [#options-and-isolation-level]

`db.transaction(...)` takes the callback and nothing else. There are no `isolationLevel`, `timeout`, or `maxWait` options; see [what is not available yet](https://www.prisma.io/docs/orm/coming-from-prisma-orm-7#not-available-yet). Every transaction runs at the database's default isolation level, `READ COMMITTED` on PostgreSQL, and you cannot ask for another level yet. If you need one, set it yourself as the first statement in the callback:

```typescript
await db.transaction(async (tx) => {
  await tx.execute(db.raw.sql`SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`.affectedCount().build());
  // your writes
});
```

There is no transaction timeout either, so a transaction stays open for as long as your callback runs. If you need a limit, set PostgreSQL's `idle_in_transaction_session_timeout` on the connection (`transaction_timeout` on PostgreSQL 17 and later).

## Roll back on errors [#roll-back-on-errors]

The transaction commits when the callback returns and rolls back when it throws. Nothing inside the callback survives an error:

```typescript
try {
  await db.transaction(async (tx) => {
    await tx.orm.public.User.create({ email: "ghost@prisma.io", name: "Ghost" });
    throw new Error("boom");
  });
} catch {
  // The user record was rolled back and does not exist
}
```

### Write conflicts [#write-conflicts]

When PostgreSQL cannot commit your transaction because another transaction touched the same rows, it aborts yours. Errors from the database arrive with a `sqlState` property holding the PostgreSQL error code (`40001` for a serialization failure, when two transactions changed the same rows and one had to give way; `40P01` for a deadlock). When the conflict surfaces at commit, the error you catch has the code `RUNTIME.TRANSACTION_COMMIT_FAILED` and the database error sits on its `cause`. There is no exported error class to check with `instanceof`, so test for the property, on the error and on its `cause`. Prisma ORM does not retry write conflicts for you, so write the retry yourself around the `db.transaction(...)` call:

```typescript
async function withRetry<T>(run: () => Promise<T>, attempts = 3): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    try {
      return await run();
    } catch (error) {
      const source = error instanceof Error && error.cause ? error.cause : error;
      const code = typeof source === "object" && source && "sqlState" in source ? source.sqlState : null;
      if ((code !== "40001" && code !== "40P01") || attempt === attempts) throw error;
    }
  }
}
await withRetry(() => db.transaction(async (tx) => { /* your writes */ }));
```

## Use the SQL builder in a transaction [#use-the-sql-builder-in-a-transaction]

The [SQL query builder](https://www.prisma.io/docs/orm/fundamentals/advanced-queries) works the same way inside a transaction. `tx.sql` has the methods `db.sql` has, and you run the finished query with `tx.execute(...)`:

```typescript
const cutoff = Temporal.Instant.from("2026-01-01T00:00:00Z");
await db.transaction(async (tx) => {
  const query = tx.sql.public.post
    .update({ published: false })
    .where((f, fns) => fns.lt(f.createdAt, cutoff))
    .build();
  await tx.execute(query);
});
```

Two things differ from `tx.orm`. Under `tx.sql.public` the property name is the table name, so it is `tx.sql.public.post` where the ORM API has `tx.orm.public.Post`. The table name is the model name with a lowercase first letter, so the model `BlogPost` has the table `blogPost`, and you set `@@map` on the model to use a different table name.
The second difference is that you chain clauses and then call `.build()` to finish the query. `.build()` returns the query without running it. `tx.execute(...)` runs it and returns `{ affectedRows }`, the number of rows it changed. The `.where(...)` callback receives two arguments: `f` holds the columns, and `fns` holds the operators (`lt` is less-than). [Advanced queries](https://www.prisma.io/docs/orm/fundamentals/advanced-queries#build-a-query-and-run-it) covers both.

## Transactions on MongoDB [#transactions-on-mongodb]

Prisma ORM does not support MongoDB transactions yet: there is no `db.transaction(...)` on the MongoDB client. To run a multi-document transaction today, use the MongoDB driver directly. Share one `MongoClient` between Prisma ORM and your code, and group the writes in a driver session.
MongoDB only runs transactions on a replica set, and a standalone server rejects them. To turn a standalone server into a single-member replica set, follow the MongoDB guide on [converting a standalone to a replica set](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/).
Install `@prisma/orm-mongo`, which a MongoDB project already has, together with version 7 of the `mongodb` driver:

  

#### bun

```bash
bun add @prisma/orm-mongo mongodb@7
```

#### pnpm

```bash
pnpm add @prisma/orm-mongo mongodb@7
```

#### yarn

```bash
yarn add @prisma/orm-mongo mongodb@7
```

#### npm

```bash
npm install @prisma/orm-mongo mongodb@7
```

`prisma orm init` writes `src/prisma/db.ts` with a `url` option. Replace that option with a `MongoClient` you create and export yourself, so your driver code and Prisma ORM use the same connection:

Run `npx prisma contract emit` once first; `db.ts` imports the two files it writes.

```typescript title="src/prisma/db.ts"
import "dotenv/config";
import mongo from "@prisma/orm-mongo/runtime";
import { MongoClient } from "mongodb";
import type { Contract } from "./contract.d"; // the two files `prisma contract emit` writes
import contractJson from "./contract.json" with { type: "json" };

export const client = new MongoClient(process.env["DATABASE_URL"]!);
export const db = mongo<Contract>({
  contractJson,
  mongoClient: client,
  dbName: "app",
});
```

Keep the `contractJson` import and the `Contract` type, and run `prisma contract emit` after every change to your contract, on PostgreSQL and on MongoDB alike.
`dbName` is now the only thing that picks the database. Set it to the database your driver code reads, so `client.db("app")` and Prisma ORM stay on the same one. An empty `dbName` is an error.

```typescript
import { client, db } from "./prisma/db";
const session = client.startSession();
try {
  await session.withTransaction(async () => {
    const database = client.db("app");
    const users = database.collection("users");
    const user = await users.insertOne({ email: "jane@prisma.io", name: "Jane" }, { session });
    const post = { title: "Hello", published: false, authorId: user.insertedId };
    await database.collection("posts").insertOne(post, { session });
  });
} finally {
  await session.endSession();
}
// Prisma ORM reads see the committed result
const jane = await db.orm.users.where({ email: "jane@prisma.io" }).first();
```

On MongoDB there is no schema segment in the path, and the property name is the collection name. The collection name is the model name with a lowercase first letter, so a model `User` is `db.orm.user`. `db.orm.users` above is the collection name; the model is `User` with `@@map("users")`.
Writes you make through the driver skip the type-checking that Prisma ORM queries get. Put each driver transaction in its own small function, and write everything around it as Prisma ORM queries.

## Coming from Prisma ORM 7 [#coming-from-prisma-orm-7]

Prisma ORM 8 has no `$transaction`. Both of the Prisma ORM 7 forms become `db.transaction(async (tx) => ...)`. The interactive form maps one for one: rename the method and query through `tx`.

```diff
- const result = await prisma.$transaction(async (prisma) => {
-   const user = await prisma.user.create({ data: { email, name } });
-   return prisma.post.create({ data: { title, authorId: user.id } });
- });
+ const result = await db.transaction(async (tx) => {
+   const user = await tx.orm.public.User.create({ email, name });
+   // published is required and has no default, so you pass it
+   return tx.orm.public.Post.create({ title, published: false, authorId: user.id });
+ });
```

The array form has no replacement of its own. Write the queries out in the callback, one after another:

```diff
- const [user, post] = await prisma.$transaction([
-   prisma.user.create({ data: { email, name } }),
-   prisma.post.create({ data: { title, authorId } }),
- ]);
+ const { user, post } = await db.transaction(async (tx) => {
+   const user = await tx.orm.public.User.create({ email, name });
+   const post = await tx.orm.public.Post.create({ title, published: false, authorId: user.id });
+   return { user, post };
+ });
```

You get the same atomicity. You also get what the array form never allowed: one query's result (here `user.id`) can feed the next query in the same transaction.

## Common mistakes [#common-mistakes]

### Side effects inside the callback [#side-effects-inside-the-callback]

Sending an email or queueing a job inside the callback looks natural, because it is right next to the write it belongs to:

```typescript
await db.transaction(async (tx) => {
  const user = await tx.orm.public.User.create({ email, name });
  await sendWelcomeEmail(user.email); // runs even if the transaction rolls back
});
```

Database writes roll back; emails don't. If a later statement throws, the record disappears but the email was already sent. Return what you need from the callback, and run the side effect after the transaction has committed:

```typescript
const user = await db.transaction(async (tx) => {
  return tx.orm.public.User.create({ email, name });
});
await sendWelcomeEmail(user.email);
```

Now the email can only go out for a user that actually exists.

### Querying through db instead of tx [#querying-through-db-instead-of-tx]

If you open a transaction but keep writing `db.orm` inside the callback:

```typescript
await db.transaction(async (tx) => {
  await db.orm.public.User.create({ email, name }); // outside the transaction
});
```

Queries on `db` run on their own connection, outside the open transaction. They commit immediately and won't roll back with the rest of the callback. Use `tx` for every query inside the callback: `tx.orm` for models, `tx.sql` and `tx.execute` for the SQL query builder.

## Prompt your coding agent [#prompt-your-coding-agent]

Projects created with `npm create prisma@latest` include the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8), instruction files for coding agents; in an existing project, run `npx prisma skills sync`. The `prisma-8` skill covers transactions. Prompts that map to each section:

* "Using the prisma-8 skill, wrap this signup flow (create user, create welcome post) in a db.transaction so both writes commit together."
* "Check this transaction callback for queries that use db instead of tx, and move the email send after the commit."
* "Refactor these two service functions so the helper takes tx as a parameter instead of opening its own transaction."
* "This project is on MongoDB. Show me the driver-session pattern for an atomic two-collection write with a shared MongoClient."

## Next [#next]

* [Write data](https://www.prisma.io/docs/orm/fundamentals/writing-data): the single-record and bulk mutations you group in a transaction.
* [Use advanced queries](https://www.prisma.io/docs/orm/fundamentals/advanced-queries) to run SQL query builder statements inside or outside transactions.

## Related pages

- [`Advanced queries`](https://www.prisma.io/docs/orm/fundamentals/advanced-queries): Use the SQL query builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM API can't express.
- [`Reading data`](https://www.prisma.io/docs/orm/fundamentals/reading-data): Fetch one record or many with Prisma ORM, then filter, select, sort, paginate, and iterate the results.
- [`Relations and joins`](https://www.prisma.io/docs/orm/fundamentals/relations-and-joins): Read related records in one query with .include(), and understand how one-to-one, one-to-many, and many-to-many relationships work.
- [`Writing data`](https://www.prisma.io/docs/orm/fundamentals/writing-data): Create, update, delete, and upsert records with Prisma ORM, one at a time or in bulk.