PostgreSQL

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
9 min read

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:

bash
npx prisma migrate dev --name init

The CLI prints three lines and then nothing, until you break it:

text
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:

text
$ 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_activity showed the pooler's server connection idle, with wait event ClientRead, 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 the public schema exists and reads the server version. PostgreSQL was idle, waiting for the next command.
  • pg_locks held 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=true to the 6543 URL did not help: the command again waited until killed, its last statement a read of Prisma's _prisma_migrations table, 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:

text
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:

.env
bash
# 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:

prisma/schema.prisma
prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}
.env
bash
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:

prisma.config.ts
ts
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:

sql
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:

prisma/schema.prisma
prisma
datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
  directUrl         = env("DIRECT_URL")
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
.env
bash
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:

sql
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';
text
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.

bash
npx prisma migrate dev --name init

On port 5432, the reproduction printed:

text
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:

bash
npx prisma migrate status
text
1 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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

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.