Prisma Migrate Dev Hangs? Fix the Supabase Pooler Stall
Reproduced on Supavisor: migrate dev waits at an early query on 6543 and migrates in a second on 5432. The fix for Prisma 6 and 7, and why the advisory lock is not the cause.
TL;DR#
prisma migrate dev prints its three loading lines and then waits, with no error and no timeout, when its URL points at Supabase's shared pooler on port 6543, which runs Supavisor in transaction mode. Give the Prisma CLI port 5432 on the same pooler host (session mode) or the direct connection: change the port, or keep 6543 for Prisma Client and add directUrl (Prisma 5 and 6) or datasource.url in prisma.config.ts (Prisma 7). The stall is not Prisma's advisory lock: in a reproduction, the command stopped long before it.
The hang: what you see#
You define a model and run your first migration:
npx prisma migrate dev --name initThe CLI prints three lines and then nothing, until you break it:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:54322"That output comes from a Stack Overflow question asked against a local Supabase stack. Its most upvoted answers, posted in 2024, are about hosted Supabase and fix the freeze with the port: "By default Supabase provides port at 6543, after changing the port to 5432 my problem solved." The asker's own URL is a different case, covered in the local section below, because port 54322 is not a pooler.
Why port 6543 stalls prisma migrate dev#
Supabase's connection guide ties the mode to the port: on the shared pooler host, 5432 reaches Supavisor in session mode and 6543 reaches Supavisor in transaction mode. The same guide sends migrations to the direct connection, because "these are single sessions and Postgres native commands", and lists four things transaction mode does not support: prepared statements, WITH HOLD cursors, session-level state (session-level advisory locks included) and query pipelining. On the Prisma side, the PgBouncer page of the Prisma 7 docs says the schema engine behind Migrate "is designed to use a single connection to the database, and does not support connection pooling with PgBouncer", that Supavisor behaves like PgBouncer, and that CLI commands such as prisma migrate dev should use a direct connection string.
Neither vendor documents why the CLI waits instead of failing. A reproduction on 6 October 2026 shows where it stops. The stack was Supavisor 2.9.13 in front of PostgreSQL 17.11, with Prisma 6.19.3, Node.js 22.23.3 and a two-model schema:
$ timeout 90 npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "supavisor:6543"
### exit code: 124 after 90s (124 = killed by timeout, i.e. still waiting)Thirty seconds into that run, the database side looked like this:
pg_stat_activityshowed the pooler's server connectionidle, with wait eventClientRead, which PostgreSQL describes as "Waiting to read data from the client". Its last statement was an early query of the engine, one that checks that thepublicschema exists and reads the server version. PostgreSQL was idle, waiting for the next command.pg_locksheld no advisory lock, and no shadow database had been created.- The same project with one change, the port from 6543 to 5432, created and applied the migration in about a second.
- Appending
?pgbouncer=trueto the 6543 URL did not help: the command again waited until killed, its last statement a read of Prisma's_prisma_migrationstable, with no lock taken. - Prisma 7.10.0, with the 6543 URL in
prisma.config.ts, stopped at the same early query.
That stack is a local Supavisor tenant, not Supabase's hosted pooler, whose version and settings cannot be inspected from outside. The hosted behaviour rests on the reports above and on Supabase's documentation.
What the advisory lock does, and what it does not#
Prisma Migrate does take a PostgreSQL advisory lock, so that two migrations cannot run at once. The advisory locking section of Prisma's docs lists migrate deploy, migrate dev and migrate resolve, and gives the lock "a 10 second timeout (not configurable)". The schema engine source runs SELECT pg_advisory_lock(72707369) under that timeout, and migrate dev reaches it only after its checks against the shadow database. A lock held by someone else therefore ends in an error, not in a silent wait. With another session holding that lock, a run on a direct connection, with no pooler, stopped with:
Error: P1002
The database server was reached but timed out.
Please try again.
Please make sure your database server is running at the configured address.
Context: Timed out trying to acquire a postgres advisory lock (SELECT pg_advisory_lock(72707369)). Timeout: 10000ms. See https://pris.ly/d/migrate-advisory-locking for details.PgBouncer does not "reset" the session after each transaction either: its configuration reference says that "when transaction pooling is used, the server_reset_query is not used", and server_reset_query_always is off by default. Session state stays on the server connection, which matters on PgBouncer (see below).
Fix: give the Prisma CLI port 5432#
Copy the strings from the Connect panel of your Supabase project rather than editing hosts by hand: Supabase warns that the pooler host cannot be composed from your region name. [POOLER-HOST] below stands for that host.
1. Change the port#
If one DATABASE_URL serves both the CLI and a long-running server, move it to session mode. That is the URL Supabase's Prisma guide uses for migrations:
# before: ...@[POOLER-HOST]:6543/postgres
DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:5432/postgres"The direct connection, db.[PROJECT-REF].supabase.co:5432 with the plain postgres user, works too, but Supabase documents it as IPv6-only unless the project has the IPv4 add-on.
2. Prisma 5 and 6: keep 6543 for the app, add directUrl#
For serverless deployments, keep the transaction pooler for Prisma Client and give the CLI its own variable:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:5432/postgres"The CLI then connects through directUrl. In the reproduction, a db push run with this block and DATABASE_URL still on 6543 printed at "supavisor:5432" in its Datasource line and applied the schema.
3. Prisma 7: the 5432 URL goes in prisma.config.ts#
Prisma 7 no longer reads connection strings from schema.prisma. Its docs call url and directUrl deprecated, and the CLI rejects them: Prisma 7.10.0 stops with error P1012, "The datasource property directUrl is no longer supported in schema files". The upgrade guide moves a former directUrl value into datasource.url of prisma.config.ts, the URL the CLI uses:
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: {
url: env("DIRECT_URL"),
},
});Your application keeps the 6543 string by passing it to the driver adapter, as in Prisma's own example: new PrismaPg({ connectionString: process.env.DATABASE_URL }).
Local Supabase (supabase start)#
Port 54322 is not a pooler. The Supabase CLI binds it straight to the database container's port 5432, as it already did when the question was asked in January 2023. The local pooler is a separate service on port 54329, which the CLI configuration template ships with enabled = false. So a freeze on 54322 is not the pooler problem, and remapping ports will not change it. The only answer posted at the time says a shadowDatabaseUrl resolved it, and nobody documents why.
To see what the database is doing while the CLI waits, query it from a second terminal:
SELECT pid, state, wait_event_type, wait_event, left(query, 80) AS query
FROM pg_stat_activity
WHERE datname IS NOT NULL AND pid <> pg_backend_pid();A row whose wait_event_type is Lock is a statement waiting for a lock that another session holds; the wait event table lists every value. Rows that are all idle with ClientRead mean PostgreSQL is waiting on the client side, as in the Supavisor run.
Self-hosted Supabase with Docker Compose#
Supabase's self-hosting compose file publishes Supavisor, not Postgres: POSTGRES_PORT (5432 by default) is session mode and POOLER_PROXY_PORT_TRANSACTION (6543) is transaction mode. The hosted rule applies unchanged: the CLI uses 5432.
When the CLI moves on: the shadow database (P3014)#
migrate dev replays your migrations in a temporary shadow database, which it creates and drops on each run. If the role in your URL may not create databases, the command stops with error P3014, whose message begins "Prisma Migrate could not create the shadow database. Please make sure the database user has permission to create databases."
On Supabase, the default postgres role has that privilege: Supabase's Postgres image runs ALTER ROLE postgres NOSUPERUSER CREATEDB CREATEROLE LOGIN REPLICATION BYPASSRLS;, and Supabase's Prisma guide creates its custom prisma user with bypassrls createdb. P3014 there points at a custom role created without it. The shadow database docs name Heroku, Digital Ocean and Vercel Postgres as cloud hosts where you may have to create the shadow database yourself. You create it once and name it in shadowDatabaseUrl, in schema.prisma on Prisma 6 and in the datasource object of prisma.config.ts on Prisma 7:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}SHADOW_DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:5432/postgres_shadow"Create postgres_shadow first, for example with CREATE DATABASE postgres_shadow; in psql, and never point shadowDatabaseUrl at the database that holds your data: Prisma resets that database at the start of every migrate dev. The Heroku case is walked through in Prisma Migrate P3014: permission denied to create database.
Why prisma db push is not a way around it#
db push skips the advisory lock: in the engine source, only applying migrations and migrate resolve take it, and db push does neither. That does not save it from the pooler. In a reproduction on 5 October 2026 with the same Supavisor stack, npx prisma db push through port 6543 waited until killed on Prisma 6.19.3, with or without ?pgbouncer=true, and on Prisma 7.10.0 without it, and prisma db pull behaved the same on 6.19.3. It also leaves no migration files and no _prisma_migrations table behind: Prisma's db push reference describes it as "a good choice when you don't need to version schema changes", so a later migrate deploy has no history to apply. Switching commands changes nothing here: the port has to change. The db push runs from that reproduction, and the same port fix written for Prisma 6 and 7, are in Fix npx prisma db push hanging on Supabase's port 6543.
On PgBouncer 1.25: it runs, but the lock outlives the command#
PgBouncer 1.25.2 behaved differently in the same reproduction. In transaction mode, db push and migrate dev completed, with max_prepared_statements at 200 and at 0. The lock outlived the command, though: PgBouncer kept the server connection, and with it the lock. This query, run straight against PostgreSQL after the migration, shows it:
SELECT l.locktype, l.objid, l.pid, l.granted, a.application_name, a.state
FROM pg_locks l LEFT JOIN pg_stat_activity a USING (pid)
WHERE l.locktype = 'advisory';locktype | objid | pid | granted | application_name | state
----------+----------+------+---------+------------------+-------
advisory | 72707369 | 2389 | t | | idle
(1 row)PostgreSQL holds a session-level advisory lock "until explicitly released or the session ends" (explicit locking docs), and that pooled session did not end. The next migrate dev reused the same server connection and passed. A third run, started while another client of the same PgBouncer held that connection in a transaction, got a different one and failed after 11 seconds with the P1002 advisory lock error shown above. If you run Migrate through PgBouncer and see that error, the query above names the server connection that holds the lock. The lasting fix is the same as on Supabase: a direct connection for the CLI, which is what Prisma's PgBouncer page prescribes.
Verify the migration runs#
Run the migration again and read the Datasource line first: it shows the host and port the CLI actually used.
npx prisma migrate dev --name initOn port 5432, the reproduction printed:
Datasource "db": PostgreSQL database "postgres", schema "public" at "supavisor:5432"
Applying migration `20261006000744_init`
The following migration(s) have been created and applied from new schema changes:
prisma/migrations/
└─ 20261006000744_init/
└─ migration.sql
Your database is now in sync with your schema.Prisma 6 then regenerates Prisma Client itself. The Prisma 7 migrate dev reference says it "no longer automatically triggers prisma generate", so run that yourself on Prisma 7. To check the history without changing anything:
npx prisma migrate status1 migration found in prisma/migrations
Database schema is up to date!If the error changes#
Once the CLI is off port 6543, the application is the part still using it. If Prisma Client queries on Prisma 5 or 6 start failing with prepared statement "s0" already exists, the pooled URL lacks ?pgbouncer=true, which Supabase's Prisma troubleshooting page prescribes because transaction mode does not support prepared statements. prepared statement s0 already exists: Fix for Prisma covers that error. If migrate dev fails outright with FATAL: bouncer config error instead of waiting, the URL reaches a PgBouncer, as Supabase's port 6543 did in 2022: Fix: Can't Migrate Schema Using Prisma With Supabase covers that variant. And if Prisma rejects the edited string with invalid port number although the port is right, a special character in the password is usually breaking the URL: Prisma Can't Connect to PostgreSQL: Fix invalid port shows the percent-encoding.
The models were never the problem. On Supabase's hosted pooler, the CLI was talking to a pooler built for short application transactions, and giving it port 5432, or a direct connection, is the whole fix.
Further reading: Fix Prisma 'Unique constraint failed on the fields: (id)'.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix npx prisma db push Hanging on Supabase (Port 6543)
Reproduced against Supavisor: the same schema waits until killed on port 6543 and syncs on 5432. The port, directUrl and prisma.config.ts fixes, version by version.
Fix: Can't Migrate Schema Using Prisma With Supabase
The message comes from PgBouncer, and the same mistake now shows up as a silent hang: which port Prisma Migrate needs on Supabase, and how to keep both URLs in Prisma 6 and 7.
Fix Prisma 'Unique constraint failed on the fields: (id)'
Prove in two queries that the id sequence is stuck behind your imported rows, reset it safely even on an empty table, and see why retrying seems to fix it.


