PostgreSQL

Fix drizzle-kit migrate Hanging After Using 'pg' Driver

Get a real error message, or a finished migration, out of a drizzle-kit migrate that sits on its last line: two causes you can rule out in a minute, and two ways to apply the migrations anyway.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

drizzle-kit migrate prints its config lines, then Using 'pg' driver for database querying, and nothing else. The report is drizzle-orm issue #4451. It was filed on drizzle-kit 0.31.0 and is still open, and the Drizzle team has not confirmed a root cause. Before you blame the bug, rule out two ordinary failures that also stop without an error message:

  • The database never answers. drizzle-kit creates its pg pool without a connection timeout, so the spinner keeps turning.
  • The migration failed, but the error was not shown. Since drizzle-kit 0.31.10, a failed migrate prints only the spinner line and exits with code 1.

Both print one more line than the issue quotes, the applying migrations... spinner.

Either way, the way out is the same. Run the drizzle-orm migrator directly: it is the function the CLI calls, it prints the real error, and you can give it a timeout. Or fall back to the 0.30 line, as several reporters did with the range ^0.30.5, which installs 0.30.6 when no lockfile holds an older version.

The output that stops#

This is the output from the original report. PostgreSQL logging was on with log_statement = 'all', and it recorded no connection from this process:

text
Reading config file '/path/to/your/drizzle.config.ts'
[drizzle.config.ts] DATABASE_URL: postgresql://user:********@host:port/db
Using 'pg' driver for database querying
(...hangs here...)

The reporter was on macOS (Apple Silicon), Node.js v20.19.1, drizzle-kit 0.31.0, drizzle-orm 0.43.0 and pg 8.15.5. Their own checks ruled out a lot:

  • psql connected with the same DATABASE_URL.
  • A minimal pg script queried fine.
  • drizzle-kit push worked.
  • The hang happened only when .sql files were in the migrations folder.

Later comments on the issue report the same stall on Bun, on Windows 11 against Supabase, and on macOS against Neon with drizzle-kit 0.31.x.

What is known, and what is not#

The fix the issue's readers agree on is a downgrade. One commenter wrote, and others confirmed with reactions:

I also fixed it by using "drizzle-kit": "^0.30.5" instead of "drizzle-kit": "^0.31.4".

Three other reports on the thread are single data points. One user saw the expected migrations applied successfully! when the same command ran in a JavaScript Debug Terminal. Another fixed it by removing nvm and using a Homebrew Node. A third, on a Linux VPS, traced a similar stall to the machine running out of memory. None of these explains the bug, and none has a maintainer's confirmation.

The hang does not happen on every machine. In a reproduction on Node.js v20.20.2 (Debian 12 container) against PostgreSQL 17.11, on 10 October 2026, the issue's exact versions applied a migration and exited with code 0. So did the current latest release on npm, drizzle-kit 0.31.11, on both a first run and a second run. A traced re-run on a fresh database confirmed that stdout was piped, not attached to a terminal:

text
--- drizzle-kit 0.31.0 (drizzle-orm 0.43.0, pg 8.15.5)
$ DATABASE_URL=postgresql://postgres:secret@db:5432/app npx drizzle-kit migrate | strip
stdout: piped
No config path provided, using default 'drizzle.config.ts'
Reading config file '/k0.31.0/drizzle.config.ts'
Using 'pg' driver for database querying
[⣷] applying migrations...[✓] migrations applied successfully!
exit=0

The macOS-specific stall was not reproduced here. What was reproduced are two ordinary failures that also give no error message. Unlike the issue's output, both show the applying migrations... spinner after the driver line. You can check for both before you change versions.

Why a failing migrate can look like a hang#

The pg pool has no connection timeout#

The drizzle-kit migrate docs list the order of work: read the .sql files, then connect and fetch the applied-migration log, then pick and run the new ones. For PostgreSQL with the pg package installed, drizzle-kit builds the pool in its bundled bin.cjs like this, with no timeout option:

js
const client = "url" in credentials2 ? new pg.Pool({ connectionString: credentials2.url, max: 1 }) : new pg.Pool({ ...credentials2, ssl, max: 1 });

The node-postgres pool documentation says of the connection timeout: "By default this is 0 which means no timeout." If the host does not answer (a firewall dropping packets, a private IP with no route, a VPN that is down), the connect call waits for the operating system to give up, and the spinner turns for that whole time. With DATABASE_URL pointed at an address where no PostgreSQL runs, drizzle-kit 0.31.11 was still spinning when timeout killed it after 15 seconds:

text
$ DATABASE_URL=postgresql://postgres:secret@10.255.255.1:5432/app timeout 15 npx drizzle-kit migrate
No config path provided, using default 'drizzle.config.ts'
Reading config file '/app/drizzle.config.ts'
Using 'pg' driver for database querying
[⣷] applying migrations...[⣯] applying migrations...[⣟] applying migrations...[⡿] applying migrations...[⢿] ap
exit=124 after 15 s

Since 0.31.10, the error is not printed#

drizzle-kit's migrate handler wraps the migration in a progress view and has its own catch that runs console.error(e) and exits with 1. In drizzle-kit 0.31.10 and 0.31.11, the bundled terminal helper catches the rejection first and exits before that catch runs:

js
function renderWithTask7(view5, task) {
  return __awaiter2(this, void 0, void 0, function* () {
    const terminal = new TaskTerminal(view5, process.stdout);
    terminal.requestLayout();
    try {
      const result = yield task;
      terminal.clear();
      return result;
    } catch (err2) {
      terminal.reject(err2);
      process.exit(1);
    }
  });
}

terminal.reject(err2) asks the migrate view to render its rejected state. That view only knows how to draw the spinner:

js
MigrateProgress = class extends import_hanji.TaskView {
  // constructor (spinner timer) omitted
  render(status) {
    if (status === "pending" || status === "rejected") {
      const spin = this.spinner.value();
      return `[${spin}] applying migrations...`;
    }
    return `[${source_default.green("✓")}] migrations applied successfully!`;
  }
};

The same analysis is in issue #5601, filed against the 1.0 beta. The published tarballs show where it starts in the 0.31 line. The catch is absent in 0.31.0 and 0.31.9 and present from 0.31.10, the release whose bundled hanji terminal library moved from ^0.0.5 to ^0.0.8. On npm, the hanji package has that try/catch in its renderWithTask in 0.0.6, 0.0.7 and 0.0.8, and not in 0.0.5:

text
drizzle-kit 0.31.0: 'terminal.reject(err' occurrences=0  hanji devDependency=^0.0.5
drizzle-kit 0.31.9: 'terminal.reject(err' occurrences=0  hanji devDependency=^0.0.5
drizzle-kit 0.31.10: 'terminal.reject(err' occurrences=1  hanji devDependency=^0.0.8
drizzle-kit 0.31.11: 'terminal.reject(err' occurrences=1  hanji devDependency=^0.0.8
hanji 0.0.5: 'terminal.reject(err' occurrences=0
hanji 0.0.6: 'terminal.reject(err' occurrences=1
hanji 0.0.7: 'terminal.reject(err' occurrences=1
hanji 0.0.8: 'terminal.reject(err' occurrences=1

With a wrong password and nothing else changed, 0.31.9 names the problem, and 0.31.10 prints nothing on stderr:

text
--- drizzle-kit 0.31.9 (drizzle-orm 0.45.2)
$ DATABASE_URL=postgresql://postgres:wrong@db:5432/app npx drizzle-kit migrate
[stdout]
Using 'pg' driver for database querying
[⣷] applying migrations...
[stderr]
DrizzleQueryError: Failed query: CREATE SCHEMA IF NOT EXISTS "drizzle"
params: 
  cause: error: password authentication failed for user "postgres"
exit=1
--- drizzle-kit 0.31.10 (drizzle-orm 0.45.2)
$ DATABASE_URL=postgresql://postgres:wrong@db:5432/app npx drizzle-kit migrate
[stdout]
Using 'pg' driver for database querying
[⣷] applying migrations...[⣷] applying migrations...
[stderr]
exit=1

A failing SQL statement behaves the same way. With a users table already in the database and drizzle's log table dropped, 0.31.11 ended on the spinner line with exit code 1 and an empty stderr. The last thing on screen is applying migrations..., with nothing to say that the run is over or why it failed.

Fix#

1. Read the exit code#

Run the command again and print its exit status straight after:

bash
npx drizzle-kit migrate; echo "exit=$?"
  • exit=1 within seconds means the migration failed and drizzle-kit hid the error. Go to step 2 to see it.
  • No prompt back after a minute means the connection never completed, or you have the bug from issue #4451. Step 2 tells those two apart.

2. Run the drizzle-orm migrator yourself#

Option 4 of the Drizzle migrations guide applies the generated SQL from your own code with migrate() from drizzle-orm/node-postgres/migrator. That is the same function drizzle-kit migrate calls for pg. Save this as migrate.mjs next to drizzle.config.ts, and point migrationsFolder at the out folder from your config:

js
import pg from "pg";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
 
const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 1,
  connectionTimeoutMillis: 10_000, // fail after 10 s instead of waiting on the OS
});
 
try {
  await migrate(drizzle(pool), { migrationsFolder: "./migrations" });
  console.log("migrations applied");
} catch (err) {
  console.error("migration failed:", err.cause ?? err);
  process.exitCode = 1;
} finally {
  await pool.end();
}

Run it with the same .env loader the issue used:

bash
npx dotenv-cli -e .env -- node migrate.mjs

Against the three failures from the reproduction, the script named each cause:

text
$ DATABASE_URL=postgresql://postgres:wrong@db:5432/app node migrate.mjs
migration failed: error: password authentication failed for user "postgres"
$ DATABASE_URL=postgresql://postgres:secret@10.255.255.1:5432/app node migrate.mjs
migration failed: Error: Connection terminated due to connection timeout
exit=1 after 10 s
$ DATABASE_URL=postgresql://postgres:secret@db:5432/app node migrate.mjs
migration failed: error: relation "users" already exists

The last one is the SQL failure that drizzle-kit 0.31.11 left silent. The script can replace drizzle-kit migrate for good. In package.json, point the migrate script at it and keep drizzle-kit generate for writing migrations:

json
{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "dotenv -e .env -- node migrate.mjs"
  }
}

The dotenv binary in that script comes from the dotenv-cli package, so install it as a dev dependency (npm install -D dotenv-cli) for npm run db:migrate to find it.

3. Or fall back to drizzle-kit 0.30.6, pinned#

The reporters on the issue wrote the range ^0.30.5, not an exact version. For a 0.x package that caret range covers every 0.30 release from 0.30.5 up, so a fresh install without a lockfile gets the newest one on the npm registry, 0.30.6. It was published on 27 March 2025, before any of those comments. The registry lists both versions as matches:

text
$ npm view "drizzle-kit@^0.30.5" version
drizzle-kit@0.30.5 '0.30.5'
drizzle-kit@0.30.6 '0.30.6'

Pin the version the range resolves to, so a later install cannot move it:

bash
npm install -D drizzle-kit@0.30.6 --save-exact

0.30.6 predates the silent catch. In the reproduction, with drizzle-orm 0.45.2, it migrated a fresh database, and with a wrong password it printed the cause:

text
--- drizzle-kit 0.30.6 (drizzle-orm 0.45.2, pg 8.23.1)
$ DATABASE_URL=postgresql://postgres:secret@db:5432/app npx drizzle-kit migrate | strip
stdout: piped
No config path provided, using default 'drizzle.config.ts'
Reading config file '/k0.30.6/drizzle.config.ts'
Using 'pg' driver for database querying
[⣷] applying migrations...[✓] migrations applied successfully!
exit=0

That proves the version works on a healthy database. It does not prove 0.30.6 avoids the macOS hang, which this reproduction never triggered: that part rests on the reporters' word. A downgrade also gives up whatever later 0.31 releases fixed, and the pool still has no connection timeout. Treat it as a stopgap and keep step 2 for CI.

Verify the fix#

A run that worked ends with exit code 0 and leaves one row per applied migration in drizzle's log table. Its default home is the drizzle schema:

bash
psql "$DATABASE_URL" -c 'select id, hash, created_at from drizzle.__drizzle_migrations order by id;'

Run the migration a second time. With nothing pending, it should finish at once and add no new row. In the reproduction, two runs of migrate.mjs on a fresh database left exactly one row for the single migration file.

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.

When the stall only happens in CI or Docker#

A related report, issue #5679, blames a stdout that is not a terminal (a Docker entrypoint, a CI job, | cat), and PR #5950 proposes to skip the spinner in that case. The PR is open, not merged. In the traced re-run, drizzle-kit 0.31.0, 0.31.11 and 0.30.6 each reported a piped stdout and each finished on a healthy database. So in a container, suspect the network first: a DATABASE_URL that says localhost (or the loopback IP) inside a container dials that container itself, as explained in the Docker Compose ECONNREFUSED fix. If nothing at the address answers, the error does not come at once: pg waits until the operating system gives up on the connection. The pg default behind that wait is the same one that makes Sequelize's authenticate() hang with no success or error. A healthcheck that reports healthy does not prove the job can reach the database: the GitHub Actions version of the error comes from a job container dialling its own loopback.

Once the migrator prints a real error, fix that error rather than drizzle-kit. If it reads password authentication failed for user "postgres", continue with the PostgreSQL password authentication fix: it explains how a role with no password, an md5 hash under a scram-sha-256 rule, or a Docker volume holding an older password produces that message. If it is a SQL error such as relation "users" already exists, the database holds tables that drizzle's migration log does not know about. Bring the two back in line before you migrate again, either by restoring the log table or by starting from an empty database. And if the migrator applies everything on a database where drizzle-kit migrate stalled, you are looking at the unexplained hang from issue #4451: keep the script, and follow the issue for a fix.

Further reading: Drizzle: Offset is outside the bounds of the DataView. Also see drizzle-seed reset() fails on drizzle-orm beta/rc: fix. Also see drizzle-kit migrate: relation "users" already exists. Also see Drizzle: Cannot destructure property 'resourceArn' fix. Also see Drizzle db.query drops pgSchema: relation does not exist. Also see Fix NeonDbError: Error connecting to database: fetch failed, drizzle-kit: Cannot use import statement outside a module.

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