You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@prisma/client + prisma + @prisma/adapter-libsql, reproduced on 7.9.1 and 7.10.0 (latest stable 7.x). I could not test against Prisma 8: @prisma/adapter-libsql has no published 8.0.0 release — its version list jumps straight from 7.10.0 to unstable 8.1.0-dev.* snapshots — so there's currently no stable/RC Prisma-8 + libSQL-adapter combination to verify against.
What happened?
Every prisma.$transaction(...) call permanently leaks a small, fixed amount of native (non-V8) memory — invisible to process.memoryUsage().heapUsed / .external / .arrayBuffers, which all stay completely flat — and only visible as steadily climbing RSS. --expose-gc + an explicit global.gc() between every call does not reclaim it.
Minimal isolated proof, same exact query, same exact client/connection, run 500 times each way:
Bare (prisma.widget.findFirst(...), unwrapped): RSS flat, ~118–121MB the whole run.
Wrapped (prisma.$transaction(async (tx) => { await tx.widget.findFirst(...) })): RSS climbs steadily and does not plateau — 105MB → 184MB over 500 calls on 7.9.1 (~0.16MB/call), and the same ~0.16MB/call rate on 7.10.0 (119MB → 200MB).
The transaction body here does a single read and nothing else — there is nothing for an application to be holding onto. The leak reproduces identically against a fresh, empty SQLite file with a single trivial model.
This isn't specific to a toy case, either — I found it while diagnosing a real Next.js app's dev server growing to ~12GB RSS over a multi-hour session. Its own world-simulation tick wraps most of its writes in $transaction() (financial charges, settlement, etc.), and bisecting which of ~9 systems was "the" leak showed it wasn't any one of them — every system that called $transaction() contributed roughly in proportion to how many transactions it opened per tick. The leak is the sum across every $transaction() call made over the process's lifetime, not tied to any particular query or table.
What did you expect to happen?
RSS to plateau (or at least not grow unboundedly) once the JS heap and Node's own tracked external/arrayBuffers memory are stable — an interactive $transaction() that opens, does one read, and commits should leave no permanent trace once it resolves.
Database: SQLite (local file), via @prisma/adapter-libsql
Reproduced on prisma/@prisma/client/@prisma/adapter-libsql7.9.1 and 7.10.0 (identical ~0.16MB/transaction-call leak rate on both)
Additional context
Related but distinct: the 7.9.0 changelog fixed a connection leak specifically on an interactive transaction that times out (maxWait) while starting. This is not that — every transaction here starts and commits successfully, quickly, with no timeout involved.
Given the leak is invisible to heapUsed/external/arrayBuffers, I'd guess it's native memory held by the query-compiler/interpreter layer per transaction (@prisma/client-engine-runtime) rather than anything in the JS driver-adapter glue, but I don't have visibility into that layer to confirm.
Package and version
@prisma/client+prisma+@prisma/adapter-libsql, reproduced on 7.9.1 and 7.10.0 (latest stable 7.x). I could not test against Prisma 8:@prisma/adapter-libsqlhas no published8.0.0release — its version list jumps straight from7.10.0to unstable8.1.0-dev.*snapshots — so there's currently no stable/RC Prisma-8 + libSQL-adapter combination to verify against.What happened?
Every
prisma.$transaction(...)call permanently leaks a small, fixed amount of native (non-V8) memory — invisible toprocess.memoryUsage().heapUsed/.external/.arrayBuffers, which all stay completely flat — and only visible as steadily climbing RSS.--expose-gc+ an explicitglobal.gc()between every call does not reclaim it.Minimal isolated proof, same exact query, same exact client/connection, run 500 times each way:
prisma.widget.findFirst(...), unwrapped): RSS flat, ~118–121MB the whole run.prisma.$transaction(async (tx) => { await tx.widget.findFirst(...) })): RSS climbs steadily and does not plateau — 105MB → 184MB over 500 calls on 7.9.1 (~0.16MB/call), and the same ~0.16MB/call rate on 7.10.0 (119MB → 200MB).The transaction body here does a single read and nothing else — there is nothing for an application to be holding onto. The leak reproduces identically against a fresh, empty SQLite file with a single trivial model.
This isn't specific to a toy case, either — I found it while diagnosing a real Next.js app's dev server growing to ~12GB RSS over a multi-hour session. Its own world-simulation tick wraps most of its writes in
$transaction()(financial charges, settlement, etc.), and bisecting which of ~9 systems was "the" leak showed it wasn't any one of them — every system that called$transaction()contributed roughly in proportion to how many transactions it opened per tick. The leak is the sum across every$transaction()call made over the process's lifetime, not tied to any particular query or table.What did you expect to happen?
RSS to plateau (or at least not grow unboundedly) once the JS heap and Node's own tracked
external/arrayBuffersmemory are stable — an interactive$transaction()that opens, does one read, and commits should leave no permanent trace once it resolves.Minimal reproduction
Sample output on 7.10.0,
transactionmode (baremode stays flat at ~118–121MB for comparison):Environment
@prisma/adapter-libsqlprisma/@prisma/client/@prisma/adapter-libsql7.9.1 and 7.10.0 (identical ~0.16MB/transaction-call leak rate on both)Additional context
maxWait) while starting. This is not that — every transaction here starts and commits successfully, quickly, with no timeout involved.$transaction()itself against a trivial single-row query the way this one does.heapUsed/external/arrayBuffers, I'd guess it's native memory held by the query-compiler/interpreter layer per transaction (@prisma/client-engine-runtime) rather than anything in the JS driver-adapter glue, but I don't have visibility into that layer to confirm.