Prisma Client $transaction: atomic multi-model writes
Wrap related Prisma Client writes so they commit together or roll back together, with worked examples and limits.
03 Jul 2026, 20:04 UTC

The problem and the useful answer
You create a user record, then create a related profile record. If the profile creation fails, the user record stays in the database and you have an orphan row. The useful takeaway is to wrap related writes in Prisma Client's $transaction so every operation commits together, or none of them do.
$transaction is a Prisma Client method that groups multiple database operations into one atomic unit. 'Atomic' means the database treats the group as a single indivisible action: either every change is persisted, or the database is returned to its previous state.
How the mechanism works
Prisma Client offers two forms. The array form accepts a list of Prisma Client requests. The interactive form accepts an async function that receives a transaction client, usually named tx. Both forms share the same database transaction. If any operation throws, Prisma Client rolls back the entire group.
The array form runs operations sequentially, not in parallel. The interactive form is useful when a later operation depends on an earlier result, or when you need to read data inside the transaction boundary.
Worked configuration: schema and script
Assume this Prisma schema, saved as prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
user User @relation(fields: [userId], references: [id])
userId Int
}
Generate the client with npx prisma generate, then run a script in your project root:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Array form: two related writes, one transaction
await prisma.$transaction([
prisma.user.create({
data: { email: '[contact removed]' },
}),
prisma.profile.create({
data: {
bio: 'Software engineer',
user: { connect: { email: '[contact removed]' } },
},
}),
]);
// Interactive form: read inside the transaction, then decide
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: '[contact removed]' },
});
await tx.profile.create({
data: { bio: 'Designer', userId: user.id },
});
});
await prisma.$disconnect();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
If either create call fails, Prisma Client rolls back both records. The database should contain neither the user nor the profile.
Array form versus interactive form
| Form | Best for | Key behavior |
|---|---|---|
| Array of requests | Independent writes that all must succeed | Runs sequentially; no shared transaction variable |
| Interactive function | Conditional logic, dependent writes, reads inside the transaction | Callback receives a tx client bound to the same transaction |
Limits and common mistakes
- Not parallel: The array form looks like a batch, but Prisma Client executes the requests one after another.
- No nesting: Calling
prisma.$transactioninside an active transaction is not a supported pattern. - Connection pressure: Long-running interactive transactions hold a database connection for the duration of the callback. Avoid external API calls or slow loops inside the function.
- MongoDB requirement: Multi-document transactions require a replica set or sharded cluster. Standalone MongoDB instances do not support them.
- Validation is still your job:
$transactionrolls back on error, but it does not validate input before the write begins.
A common mistake is throwing an error after the first write and assuming the first record survived. It will not; the entire transaction is rolled back.
Verify the installed behavior
Prisma Client behavior can vary by version and provider. Confirm your setup before relying on it in production.
- Open a terminal in your project root and run
npx prisma --version. Record the CLI version. - Inspect
node_modules/@prisma/client/package.jsonand check theversionfield to confirm the generated client matches the CLI. - Create a test script that writes two related records inside
prisma.$transaction, then throws an error after the first operation. - Query the database after the failed transaction. Neither record should exist.
Permissions: The database user in DATABASE_URL needs create, read, and transaction privileges on the target schema. Risk: a failed script can leave partial state if your provider or transaction configuration does not fully support rollback, so test in a non-production database first.
Rollback and checking the result
Because $transaction changes state, rollback is automatic: any thrown error inside the transaction causes Prisma Client to issue a rollback, and the database returns to the state before the transaction began. To verify, count the relevant rows before and after a deliberately failing script. The count should be unchanged.
This draft describes well-established Prisma Client behavior. Verify the exact $transaction signature and provider support against your installed Prisma version before production use.