# ECONNREFUSED 127.0.0.1:6379 After Deploy: Why and How to Fix

> ECONNREFUSED 127.0.0.1 means your app is calling itself. Why it happens after deploy with Redis (6379) and Postgres (5432), and how to fix each cause.

1. [Home](/)
2. [Blog](/blog)
3. ECONNREFUSED 127.0.0.1: Your App Is Calling Itself

DeploymentOctober 7, 20269 min read

# ECONNREFUSED 127.0.0.1: Your App Is Calling Itself

Error: connect ECONNREFUSED 127.0.0.1:6379 after a deploy means your app is calling its own machine. Why the address says localhost, the ::1 variant, and how to fix each cause for Redis and Postgres.

## The short version

ECONNREFUSED means the target host answered and said nothing is listening on that port. When the address is 127.0.0.1 or ::1, your app is trying to reach a database on its own machine, usually because the connection string never reached the deployed environment, localhost was hardcoded, or the app runs in a container where localhost is the container itself. Set REDIS\_URL or DATABASE\_URL to the real host and fail loudly when it is missing.

[Teo Marquardt](/about#author)· Facts last checked October 5, 2026

It worked on your laptop. You deployed, the app started, and the first request to the cache produced this:

text

```
Error: connect ECONNREFUSED 127.0.0.1:6379
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net) {
  errno: -111,
  code: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 6379
}
```

Most answers to this error tell you to start Redis. On your laptop that's correct, and it's why the advice is everywhere. On a server it misses the point, because the most useful part of the message is the address. `127.0.0.1` is the machine the app is running on. The app isn't failing to reach your Redis; it's trying to reach a Redis on its own machine, and there isn't one. Below is why that happens, the `::1` version of the same error, what it means when the host is right and the connection is still refused, and how to tell which case you have in about a minute.

## What ECONNREFUSED means

ECONNREFUSED is an operating system error, not a Redis or Postgres one. Your process sent a TCP connection request, a machine at that address answered, and the answer was a reset: nothing is listening on that port. The [Node.js documentation](https://nodejs.org/api/errors.html#common-system-errors) describes it as a connection that could not be made because the target machine actively refused it. Note what that rules out. The network path works and the host exists; only the port is empty. (A firewall configured to reject rather than drop can produce the same refusal, but that's rare on a hosting platform.)

That makes it different from the errors it gets confused with, and the difference tells you where to look:

| Error                                  | What happened                                                | Where to look                                                                     |
| -------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| ECONNREFUSED                           | The host answered: nothing listens on this port              | The address and port in the error. Is the service running there at all?           |
| ETIMEDOUT                              | No answer before the timeout                                 | Firewall, wrong private address, a network the app can't reach                    |
| ENOTFOUND                              | The hostname didn't resolve                                  | A typo in the host, or an internal hostname used from outside its network         |
| ECONNRESET                             | The connection was accepted, then dropped                    | Server restart, TLS mismatch, idle connection killed by a proxy                   |
| FATAL: sorry, too many clients already | Postgres accepted the TCP connection and refused the session | Connection limits. [A different problem](/blog/postgres-too-many-clients-already) |

The same reset is behind every "connection refused" you have seen, from SSH and browsers too. When an app talks to its database, though, the address in the message usually gives the cause away.

Read the address before anything else

If the error says `127.0.0.1` or `::1` and your database lives somewhere else, stop debugging the database. The app is calling itself, and the fix is in its configuration.

## Why the address says 127.0.0.1

### The connection string never arrived

This is the most common cause after a first deploy. Redis and Postgres clients have defaults, and the defaults point at the local machine. `createClient()` in node-redis and `new Redis()` in ioredis both connect to `localhost:6379` when they get no options. node-postgres falls back to `localhost:5432` when neither a connection string nor the `PGHOST` variable is set. So code like this works on a laptop where Redis is installed, and quietly falls back to the default on a server where `REDIS_URL` was never set:

javascript

```
// Works locally. On a server without REDIS_URL, url is undefined
// and the client silently connects to localhost:6379.
const redis = createClient({ url: process.env.REDIS_URL });
```

With ioredis the log often shows `[ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379` repeating, because the client keeps retrying the wrong address. The variable is missing for ordinary reasons: it was in a local `.env` file that is not deployed, it was set on the old host and not the new one, or it was set on the web service and not on the worker. Moving off a platform that injected these values for you is a classic trigger. Heroku set `DATABASE_URL` and `REDIS_URL` automatically when you attached an add-on, and [the first deploy elsewhere is where you find out](/blog/migrate-from-heroku) which ones your code relied on.

The fix is to stop the default from being reachable at all. Read the variable once, at startup, and refuse to boot without it:

javascript

```
function requireEnv(name) {
  const value = process.env[name];
  if (!value) {
    console.error(`${name} is not set; refusing to start`);
    process.exit(1);
  }
  return value;
}

const redis = createClient({ url: requireEnv("REDIS_URL") });
const pool = new Pool({ connectionString: requireEnv("DATABASE_URL") });
```

A crash at boot with a clear message is a better outcome than a running app that fails on its first cache call. The deploy fails, the log names the missing variable, and on a platform that keeps the old release until the new one is healthy, the old version goes on serving.

### localhost was hardcoded for your laptop

The second cause is the same thing written directly into code or config: `host: "localhost"` in a database config file, `redis://127.0.0.1:6379` as a fallback after `||`, a `config/production.json` copied from development. Search the repository for `localhost` and `127.0.0.1`. A fallback such as `process.env.REDIS_URL || "redis://localhost:6379"` is the most common version, and it causes the same quiet failure as no variable at all.

### Inside a container, localhost is the container

In Docker, each container has its own network namespace, so `localhost` inside the app container means the app container. A Redis running in another container, or on the host, is not there. This is why `connect ECONNREFUSED 127.0.0.1:5432` fills the Docker forums: Postgres runs fine, `psql` from the host connects fine, and the app in its container still can't reach it. In Docker Compose, services reach each other by service name:

yaml

```
services:
  app:
    build: .
    environment:
      REDIS_URL: redis://cache:6379        # the service name, not localhost
      DATABASE_URL: postgresql://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy   # wait until Postgres accepts connections
  cache:
    image: redis:7
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 2s
      retries: 15
```

If the database runs on your machine and only the app is containerised, `host.docker.internal` reaches the host on Docker Desktop. On Linux you need to add it yourself with `extra_hosts: ["host.docker.internal:host-gateway"]`. On a hosting platform the same principle applies with different names: the app has to use the hostname the platform gave the database, never a loopback address.

### ECONNREFUSED 127.0.0.1:5432: the same mistake with Postgres

Postgres shows the same mistake in its own words. From Node it looks like the Redis error with a different port; from libpq-based tools such as `psql` or psycopg2 it reads differently:

text

```
# Node (pg, Sequelize, TypeORM)
Error: connect ECONNREFUSED 127.0.0.1:5432

# libpq-based clients: psql, psycopg2, Ruby pg, PHP pgsql
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: Connection refused
	Is the server running on that host and accepting TCP/IP connections?
```

The diagnosis is identical. Check that `DATABASE_URL` is set in the environment the process actually runs in. Check also that it points at port 5432 or at a pooler port such as 6432 deliberately: if the string uses 6432 and no [connection pooler is listening there](/blog/pgbouncer-connection-pooling), you get the same refusal with the right host.

## ECONNREFUSED ::1: when localhost stopped meaning 127.0.0.1

A variant that confuses people because nothing in their code changed:

text

```
Error: connect ECONNREFUSED ::1:6379

# libpq
connection to server at "localhost" (::1), port 5432 failed: Connection refused
```

`::1` is the IPv6 loopback address. Since Node.js 17, `localhost` is resolved in the order the system resolver returns, and many systems return `::1` first, where older Node versions put IPv4 first. Redis and Postgres are often configured to listen on IPv4 only, so a connection to `localhost` that worked on Node 16 is refused on Node 17 to 19\. Node.js 20 and later turn on `autoSelectFamily` by default and fall back to the other address family, which hides the problem in many setups. As of writing, some clients still resolve the address themselves, and on Node 20+ the same failure can surface as an `AggregateError` with code `ECONNREFUSED`.

Locally, the fix is to write `127.0.0.1` instead of `localhost`, or to make the server listen on both addresses. In production it's the same fix as before. A deployed app shouldn't be connecting to loopback at all, so `::1` in a production log means the connection string is missing or hardcoded.

## The host is right and it's still refused

If the address in the error is the real database host, the app is configured correctly and the database side isn't listening at that moment. Four causes cover most cases.

* The app started before the database was ready. Docker Compose `depends_on` waits for the database container to start, not for the database inside it to accept connections. Postgres needs a few seconds to initialise on first run, and an app that connects during that window gets refused. Add a `healthcheck` to the database and use `depends_on` with `condition: service_healthy`, as described in Docker's [startup order documentation](https://docs.docker.com/compose/how-tos/startup-order/).
* The database restarted. A restart, failover or upgrade closes the port for a few seconds. Connections in flight drop with `ECONNRESET`, and new ones are refused until the server is back.
* The server only listens on loopback. Since Redis 6.2 the [default configuration](https://redis.io/docs/latest/operate/oss%5Fand%5Fstack/management/config/) ships with `bind 127.0.0.1 -::1`, and PostgreSQL defaults to [listen\_addresses = 'localhost'](https://www.postgresql.org/docs/current/runtime-config-connection.html). Installed from a distro package on a VM, either one refuses everyone who isn't on the same machine until you change that. The official Docker images already listen on all interfaces. Opening a server up means setting a password and TLS too.
* The port is wrong. A TLS port instead of the plain one, a pooler port with no pooler, or a port from an old connection string.

### Retry on startup instead of sleeping

The first two causes are temporary, and the right response is a short retry with backoff, not a `sleep 10` in the entrypoint that is too long on a fast day and too short on a slow one. Redis clients usually retry on their own: node-redis and ioredis keep reconnecting by default, so for them the job is to handle the `error` event and not crash on it. A Postgres pool tries once and throws. For that first query at startup, a small loop is enough:

javascript

```
async function connectWithRetry(connect, attempts = 6) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await connect();
    } catch (error) {
      if (error.code !== "ECONNREFUSED" || attempt === attempts) throw error;
      const delayMs = Math.min(500 * 2 ** attempt, 8000);
      console.warn(`database not ready (attempt ${attempt}), retrying in ${delayMs} ms`);
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }
}

await connectWithRetry(() => pool.query("SELECT 1"));
```

Keep the retry narrow. Retrying only on `ECONNREFUSED`, and only a few times, covers a database that is starting. A wrong address still fails after about 25 seconds, with the real error in the log, instead of retrying forever.

## A 60-second diagnosis

Run these from the same environment the app runs in: a shell in the container, or the platform's console for the service. Running them from your laptop tests your laptop's network, which is a different question.

bash

```
# 1. Is the variable there, and what host does it name?
printenv | grep -E 'REDIS_URL|DATABASE_URL|PGHOST'

# 2. Does something answer on that host and port?
nc -zv cache.example.eu 6379

# 3. Does the service itself respond?
redis-cli -u "$REDIS_URL" ping          # PONG (rediss:// needs redis-cli 6.2+)
pg_isready -d "$DATABASE_URL"           # accepting connections

# What plain redis-cli prints when nothing listens locally:
# Could not connect to Redis at 127.0.0.1:6379: Connection refused
```

| What you see                                           | Cause                                              | Fix                                                     |
| ------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------------- |
| Variable empty, error says 127.0.0.1 or ::1            | Connection string never reached this environment   | Set it on this service; fail at boot when it is missing |
| Variable set, error still says localhost               | Hardcoded host or a \|| fallback in code           | Search the codebase for localhost and 127.0.0.1         |
| Works from the host, refused from the container        | localhost means the container                      | Use the service name or the platform's hostname         |
| Right host, refused only at startup or after a restart | Database not ready yet                             | Healthcheck plus a bounded retry with backoff           |
| Right host, always refused                             | Wrong port, or the server only listens on loopback | Check the port; set bind or listen\_addresses           |

One related case on deploys: the brief window where an old instance has stopped and the new one isn't listening yet also produces refused connections, for callers of your app this time rather than your app's calls to a database. That one belongs to how the platform stops containers, covered in [exit code 137 and SIGTERM handling](/blog/exit-code-137).

## How Runsite handles it

On Runsite, [managed Redis comes with a ready-made connection string](/services/redis) that you copy into the environment variables of every service that uses it, web and worker alike. Services inside Runsite reach the instance over private networking, and connections are encrypted with TLS by default, so the string that works in production is a real hostname, not a loopback address. [Managed PostgreSQL](/services/postgresql) gives each database an internal hostname for your services and a separate external one for your laptop or a BI tool. If you see `127.0.0.1` in an error on Runsite, the variable is missing from that service or localhost is hardcoded in the code.

A failed Redis instance is restarted and rescheduled onto a healthy node, and connections drop while that happens, so the bounded retry above applies here as it does anywhere. Point the service's health check at an endpoint that touches the database rather than one that only proves the port is open, so a release that can't reach its data never takes traffic. The databases and services run in Germany.

## The short version

* ECONNREFUSED means a host answered and nothing listens on that port. The address in the error is the first thing to read.
* `127.0.0.1` or `::1` in a deployed app means it's calling its own machine: a missing `REDIS_URL` or `DATABASE_URL`, a hardcoded localhost, or a container where localhost is the container.
* Fail at boot when the connection string is missing, instead of letting the client fall back to its localhost default.
* `::1` after a Node upgrade is the IPv6 loopback; locally use `127.0.0.1`, in production fix the connection string.
* If the host is right, the database wasn't listening at that moment: use healthchecks and a short retry with backoff, then check the port and the server's listen address.

Related service

## Managed Redis on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Managed Redis](/services/redis)

[Back to all articles](/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does ECONNREFUSED mean?

ECONNREFUSED is an operating system error meaning the connection was actively refused: your process reached the target host, and the host replied that nothing is listening on that port. The network path and the host are fine; the port is empty. When the address in the error is 127.0.0.1 or ::1, the app is trying to connect to its own machine, which in a deployed app usually means the database connection string is missing or hardcoded to localhost.

### How do I fix an ECONNREFUSED error?

Read the address and port in the error first. If it is 127.0.0.1 or ::1 and the database runs elsewhere, set REDIS\_URL or DATABASE\_URL in the environment where the app runs, remove any localhost fallbacks in code, and in Docker Compose use the service name instead of localhost. If the address is the real database host, the database was not listening at that moment: add a healthcheck so the app starts after the database is ready, retry the first connection with backoff, and check the port and the server's bind or listen\_addresses setting.

### What is port 6379 used for?

6379 is the default port of Redis. An error such as connect ECONNREFUSED 127.0.0.1:6379 means a Redis client tried the default local address and found no Redis there. Locally, that means Redis isn't running. On a server, it usually means the client got no connection URL and fell back to its default of localhost:6379, so the fix is to pass the real Redis URL through an environment variable.

### How do I fix "connection to server at localhost (::1), port 5432 failed: Connection refused"?

::1 is the IPv6 loopback address. The client resolved localhost to IPv6, and the connection failed. psql tries every address localhost resolves to, so this usually means PostgreSQL is not running, or localhost maps only to ::1 while the server listens on IPv4\. Node.js 17 to 19 try only the first address, so there an IPv4-only server is enough to fail. Locally, connect to 127.0.0.1 instead of localhost, or set listen\_addresses in postgresql.conf to include both addresses and restart PostgreSQL. In a deployed app, a localhost address means the connection string is not set, so set DATABASE\_URL to the real database host.

Keep reading

## Related articles

[Databases10 min readPostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix ItYour app is suddenly throwing 500s and the database logs are full of "too many clients already." Here's what the error really means, how to see what's hogging your connections, and the fix that actually holds: pooling, not a bigger number.Jun 26, 2026Read](/blog/postgres-too-many-clients-already)[Deployment12 min readMigrating Off Heroku Postgres: The Dump, the Cutover and the RollbackThree ways to get a database out of Heroku Postgres, how to work out the downtime before you commit to it, a cutover order that doesn't lose writes, and the point after which rolling back stops being free.Sep 26, 2026Read](/blog/migrate-from-heroku)[Deployment10 min readExit Code 137: SIGKILL, Not Always Out of MemoryExit code 137 says a process was killed with SIGKILL. It doesn't say who sent it. How to tell a memory kill from a missed SIGTERM on deploy or a failing probe, and what fixes each one.Oct 5, 2026Read](/blog/exit-code-137)

## Your app deserves to be online

€5 of credit on signup. Deploy in under a minute. No credit card needed.

[Start deploying](https://dashboard.runsite.app/login)[View documentation](https://docs.runsite.app)

---

Source: https://runsite.app/blog/econnrefused-127-0-0-1
