PythonMastery
reference 4 min read · lesson 31 of 45 in Errors

ConnectionRefusedError: [Errno 111] Connection refused

1 · The lesson

read

What this error means

Your code reached the machine, and the machine said no. Something answered at that address — the network is fine, DNS resolved, packets arrived — but nothing is listening on that port, so the operating system on the other end refused the connection immediately.

This is the friendliest network failure you can get. A refusal is instant and unambiguous, unlike a timeout, which means nobody answered at all.

When you see it

python
Traceback (most recent call last):
  File "app.py", line 12, in <module>
    conn = psycopg.connect("postgresql://localhost:5432/shop")
ConnectionRefusedError: [Errno 111] Connection refused

Through requests, it arrives wrapped:

python
requests.exceptions.ConnectionError: HTTPConnectionPool(host='localhost', port=8000):
Max retries exceeded with url: /api/health
(Caused by NewConnectionError('<urllib3.connection.HTTPConnection object>:
Failed to establish a new connection: [Errno 111] Connection refused'))

On Windows the number differs — [WinError 10061] — with the same meaning.

Why it happens

Four causes, in the order they actually occur:

1. The service isn't running. Postgres, Redis, or your own dev server is stopped or crashed.
2. It's running on a different port. You connected to 5432; it's on 5433 because another Postgres already had 5432.
3. It's listening on the wrong interface. A server bound to 127.0.0.1 is unreachable from another container or machine — it must bind 0.0.0.0 to accept outside connections. This is the classic "works on my laptop, refuses in Docker".
4. You're in a container and used localhost. Inside a container, localhost is that container, not the host and not a sibling. You need the service name on the compose network, or host.docker.internal.

How to fix it

Check something is actually listening:

bash
ss -ltnp | grep 5432        # Linux
lsof -iTCP:5432 -sTCP:LISTEN # macOS
netstat -ano | findstr 5432  # Windows

Nothing returned means nothing is listening — start the service.

Check what it's bound to. 127.0.0.1:5432 in that output means local-only. To accept connections from elsewhere, bind 0.0.0.0:

python
# a dev server that other containers can reach
app.run(host="0.0.0.0", port=8000)
+ setup added so this can run · defines app
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

app = _AutoMock('app')

In Docker Compose, use the service name, not localhost:

yaml
services:
  api:
    environment:
      DATABASE_URL: postgresql://db:5432/shop   # "db", not "localhost"
  db:
    image: postgres:16

Fail with a message that says what to do, rather than letting the traceback escape:

python
import socket

try:
    conn = connect_to_db()
except ConnectionRefusedError:
    raise SystemExit(
        "Nothing is listening on the database port.\n"
        "  Is the container up?  docker compose ps\n"
        "  Right port?           echo $DATABASE_URL"
    )
+ setup added so this can run · defines connect_to_db
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def connect_to_db(*_a, **_kw):
    print('-> connect_to_db() called')
    return _AutoMock('connect_to_db()')

Do not retry a refusal the way you would a timeout. A refusal is a definitive answer; retrying it twenty times just delays the error by twenty seconds. Retry with backoff only during a known startup window — a container waiting for its database is the legitimate case.

When you'd actually see this in real code

  • The app container boots faster than the database container and connects before Postgres is accepting connections. Add a healthcheck and depends_on: condition: service_healthy.
  • A dev server bound to 127.0.0.1 inside Docker: the port is published, but nothing outside the container can reach it.
  • Redis or RabbitMQ restarted during a deploy and the pool held connections to a process that no longer exists.
  • Someone ran a second Postgres locally, so 5432 was taken and yours came up on 5433.
  • A firewall or security group that drops rather than rejects gives you a timeout instead — if you see a hang rather than an instant refusal, look at the firewall.

See Also