DevOps

Fix: Sequelize authenticate() Hangs With No Success or Error

Your container logs "start connect" and then nothing — no success, no error. The connection is not failing silently; it is still trying, because pg never times out by default.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

A container running a Node + Sequelize API prints start connect and then stops — no Connection has been established successfully, no Unable to connect to the database, nothing. The try/catch around sequelize.authenticate() never fires either branch, even though the exact same code connects fine outside Docker. This is the symptom behind a widely viewed Stack Overflow question, "Sequelize.authenticate() do not working (no success or error response) for google cloud sql connection inside Docker" (17 votes, over 10,800 views).

TL;DR#

sequelize.authenticate() is not failing silently — it is still running. Sequelize's Postgres dialect uses pg, and pg defaults connectionTimeoutMillis to 0, which means no timeout at all: if the TCP connection to the database never completes (wrong host, no route from the container to a Cloud SQL private IP, a typo'd Compose service name), the connect() call just keeps waiting instead of rejecting. Give it an explicit connectionTimeoutMillis so a dead connection fails fast with a real error you can log and act on, then fix why the container cannot reach the database in the first place.

plaintext
api_1  | Init db
api_1  | start connect
api_1  | Server running in development mode on port 5000

No further line ever appears — not an error, not a success message.

Why it happens#

Sequelize's Postgres connection goes through pg, which builds its connection options in connection-parameters.js:

js
if (config.connectionTimeoutMillis === undefined) {
  this.connect_timeout = process.env.PGCONNECT_TIMEOUT || 0
} else {
  this.connect_timeout = Math.floor(config.connectionTimeoutMillis / 1000)
}

and in client.js the connection-timeout timer is only armed when that value is greater than zero:

js
this._connectionTimeoutMillis = c.connectionTimeoutMillis || 0
// …
if (this._connectionTimeoutMillis > 0) {
  this.connectionTimeoutHandle = setTimeout(() => {
    con._ending = true
    con.stream.destroy(new Error('timeout expired'))
  }, this._connectionTimeoutMillis)
}

With the default of 0, that setTimeout is never scheduled. pg then relies entirely on whatever the underlying TCP stack eventually reports — and how long that takes, or whether it reports anything at all, depends on what is on the other end of the route: a host that actively refuses the connection answers fast with ECONNREFUSED; a firewall, security group or missing route that drops packets silently can leave the OS retrying for a long time before it gives up, if it gives up before the process itself is killed. Until one of those happens, the connect() promise is simply pending: not resolved, not rejected, just waiting for a reply that may or may not be coming back over the wire. That is exactly the "no success or error response" reported in the question — the question's own body says only that "everythings works fine in my local development", without saying what, specifically, happened differently there; the common explanation in cases like this is that outside Docker the connection either succeeds or fails immediately, while inside the docker-compose network the path to the target — a Google Cloud SQL instance's private IP, in the original question — may not get any response at all.

Reproduced (Node 20, pg 8.11.3, official node:20-alpine image, 8 October 2026): a pg client connecting to a non-routable test address with the default options logged nothing for over 8 seconds — still pending, no success, no error — and only reported ECONNREFUSED after 21 seconds, once the container's own network stack gave up on that unreachable route. The same client with connectionTimeoutMillis: 2000 reported timeout expired in 2 seconds flat. One option changed, nothing else — and a different network path (one that drops packets instead of answering) can take even longer than 21 seconds to resolve on its own, or never resolve at all, which is the whole reason the explicit timeout matters.

Fix#

1. Set an explicit connection timeout. Pass it through Sequelize's dialectOptions (it is forwarded verbatim to pg):

js
this._sequelize = new Sequelize({
  dialect: 'postgres',
  database: this.config[this.env].database,
  username: this.config[this.env].username,
  password: this.config[this.env].password,
  host: this.config[this.env].host,
  dialectOptions: {
    connectionTimeoutMillis: 2000,
  },
});

Now a dead connection rejects in 2 seconds with Error: timeout expired instead of hanging indefinitely — the catch block in the original question's connect() method finally has something to log.

2. Read the error that timeout now surfaces, and fix the actual route. For the Google Cloud SQL case in the question, a container cannot reach a Cloud SQL instance's private IP directly unless it is on the same VPC with private services access configured; the supported path from a container is the Cloud SQL Auth Proxy sidecar, connecting over a Unix socket or the container's own loopback address instead of the instance's own IP. If the host in dbConfig is the Cloud SQL instance's private IP and there is no proxy or VPC peering, that is the unreachable route the timeout now reports.

3. For a docker-compose service, double-check the host is the service name, not the loopback address. Inside a container, the loopback address means the container itself; the database is only reachable at the Compose service name (e.g. host: 'db'), as described for the closely related ECONNREFUSED failure mode on this site.

4. If the error becomes Please install pg package manually, that is Sequelize's own dialect loader throwing when the pg package is not resolvable from node_modules — it is listed as an optional peer dependency, not a direct one, in older Sequelize majors. Add it to package.json explicitly:

plaintext
npm install pg

That is a separate failure from the hang above: it throws immediately and synchronously, before any connection attempt, rather than leaving authenticate() pending.

Verify the fix#

Rebuild and run the stack with the timeout in place, and deliberately point host at an address the container cannot reach (as in the repro above):

plaintext
docker compose up --build

Expect a line within the configured connectionTimeoutMillis, not after a long silence:

plaintext
Unable to connect to the database: Error: timeout expired

Then point host back at the real, reachable database (the Compose service name, or the Cloud SQL Auth Proxy's socket), rebuild once more, and confirm the success path logs promptly instead, without the earlier long silence:

plaintext
Connection has been established successfully.

If the error now changes to connect ECONNREFUSED at a specific IP and port, the route exists but nothing is listening there yet — continue with fixing ECONNREFUSED in Docker Compose, which covers the loopback-address-inside-a-container trap directly. If it instead becomes bind: address already in use, that is a different, host-side port conflict covered in fixing Postgres 5432 "address already in use" in Docker.

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.

A variant: Prisma instead of Sequelize#

Prisma's own PostgreSQL connector has long accepted a connect_timeout connection-string parameter with a short, non-zero default — an explicit, fast PrismaClientInitializationError P1001 ("Can't reach database server") rather than a hang, which is the behaviour behind Prisma: Can't reach database server at database:5432 on M1. Prisma's own PostgreSQL connector reference documents a change on the newer driver-adapter path (PrismaPg, PrismaNeon, and the rest): it switched to using pg's own connection defaults instead, which includes that same un-timed default — so the exact hang described above can now surface one layer up the stack as well, unless an explicit connectionTimeoutMillis is set on the adapter. The driver and the error message differ; the underlying lesson, that "no error yet" is not the same as "no error ever", does not.

A working Docker Compose development setup for a Node API — healthchecks, depends_on, and the bind-mount layout that avoids node_modules clobbering — is covered end to end in the Docker dev environment setup guide; wiring a depends_on: condition: service_healthy there removes most of the race that leads to this hang in the first place, by not starting the API container until Postgres already accepts connections.

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