Troubleshooting
Redis & caching

Fix Redis Connection Refused in Django

Fix Redis connection refused errors from Django caches, sessions, Celery, and Channels: service status, bind addresses, Docker hostnames, TLS URLs, auth, and limits.

Problem statement

Django pages that use the cache, sessions stored in Redis, Celery task submission, or Channels start failing with:

redis.exceptions.ConnectionError: Error 111 connecting to 127.0.0.1:6379. Connection refused.

Celery workers log:

[ERROR/MainProcess] consumer: Cannot connect to redis://127.0.0.1:6379/0: Error 111 connecting to 127.0.0.1:6379. Connection refused..
Trying again in 2.00 seconds...

"Connection refused" is a precise network error: the client reached the host, but nothing accepted connections on that port. It is different from:

  • Timeout (Error 110 or Timeout connecting to server): packets are dropped, usually by a firewall or security group, or the host is unreachable
  • Name resolution errors (Error -2 connecting to redis:6379. Name or service not known): the hostname does not resolve, common in Docker when services are on different networks
  • NOAUTH or WRONGPASS: connected, but authentication failed
  • max number of clients reached: connected, but Redis refused more clients
Reaching Redis from DjangoDjango or CeleryREDIS_URLNetworkhost and portRedisbind, protected modeAuthpassword or ACLRefused means nothing is listening where the client looked, often the wrong host.

Quick answer

  1. Check Redis is running on the target host: systemctl status redis-server or docker compose ps redis.
  2. Check which address and port Redis listens on: sudo ss -ltnp | grep 6379.
  3. Match Django's URL to that address. In Docker Compose, use the service name (redis://redis:6379/0), never 127.0.0.1.
  4. For remote Redis, confirm bind includes the private interface and the firewall allows the app servers.
  5. For managed Redis with TLS, use rediss:// and the provider's port.
  6. Restart the Django and Celery processes after changing URLs.
  7. Verify with redis-cli -u "$REDIS_URL" ping from the same host or container as the app.

Step-by-step solution

1. Is Redis running?

Local service:

systemctl status redis-server --no-pager
sudo journalctl -u redis-server -n 50 --no-pager

Redis fails to start for reasons you will see in the journal: an invalid directive after editing redis.conf, a port already in use, a dir that is not writable, or a failed AOF or RDB load after a crash or full disk.

Docker Compose:

docker compose ps redis
docker compose logs --tail=50 redis

If the container restarts repeatedly, the logs show why.

2. What is Redis listening on?

sudo ss -ltnp | grep -E ':6379\b'

Examples:

LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=812,fd=6))
LISTEN 0 511     [::1]:6379    [::]:* users:(("redis-server",pid=812,fd=7))

This Redis accepts only local connections. A Django process on another server gets "connection refused" (or a timeout, depending on the firewall) even though Redis is healthy.

If nothing is listening, Redis is not running or uses another port or a Unix socket; check port and unixsocket in /etc/redis/redis.conf.

3. Match the client URL

Print what the app uses, in the environment of the failing process:

python manage.py shell -c "
from django.conf import settings
print(settings.CACHES['default'].get('LOCATION'))
print(getattr(settings, 'CELERY_BROKER_URL', None))
"
every part of the URL mattersREDIS_URL=redis://:s3cret@redis:6379/1REDIS_URL=rediss://:s3cret@cache.example.net:6380/01231Host: the Compose service name, not localhost.2Database number, not a port.3Two s: TLS, as managed Redis often requires.A wrong scheme gets a reset; a wrong host gets refuseddjango-deployment.com
Print the URL Django actually loaded, with the password masked.

Test the exact URL from the same machine or container:

redis-cli -u "redis://127.0.0.1:6379/0" ping

With a password, URLs have the form redis://:password@host:6379/0 (or redis://username:password@host:6379/0 with ACL users). Avoid putting passwords on the command line on shared machines; REDISCLI_AUTH is an alternative.

4. Docker: 127.0.0.1 inside a container is the container itself

In Docker Compose, each container has its own network namespace. 127.0.0.1 in the web container refers to the web container, where no Redis runs. Use the Compose service name:

services:
  web:
    environment:
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - redis
  worker:
    environment:
      CELERY_BROKER_URL: redis://redis:6379/1
  redis:
    image: redis:7
inside a container, localhost is the containerredis://localhost:6379✕points at the web container✕nothing listening there✕Connection refusedredis://redis:6379✓the Compose service name✓Docker DNS resolves it✓same network requiredDifferent Compose projects need a shared network to see each otherdjango-deployment.com
docker compose exec web getent hosts redis confirms resolution.

Test from inside the app container:

docker compose exec web python -c "import redis, os; print(redis.Redis.from_url(os.environ['REDIS_URL']).ping())"

depends_on only controls start order, not readiness. Redis starts quickly, but add a health check and condition: service_healthy if the app connects immediately on boot.

If Redis runs on the host and the app in Docker, 127.0.0.1 still does not work from the container. Either run Redis in Compose, or connect to the host's address on the Docker bridge, with Redis bound to that interface and firewalled from the outside.

5. Remote Redis on another server

On the Redis server, in /etc/redis/redis.conf:

bind 127.0.0.1 10.0.1.20
protected-mode yes
requirepass use-a-long-generated-password
remote Redis, safely reachablebind to the private interfacenot 0.0.0.0requirepass or ACL user setprotected-mode stays onFirewall allows app servers onlyport 6379redis-cli -h from the app hostPING returns PONGAn open Redis on the internet is compromised within hoursdjango-deployment.com
Private networking plus a password is the minimum; TLS if traffic leaves it.

10.0.1.20 is the private interface address. Protected mode rejects connections from non-loopback addresses when no password is set, which is why remote connections can fail after only changing bind. Setting a password (or ACL users) satisfies protected mode.

Allow the port only from app servers:

sudo ufw allow from 10.0.1.10 to any port 6379 proto tcp
sudo ufw allow from 10.0.1.11 to any port 6379 proto tcp

Restart Redis and test from an app server:

redis-cli -h 10.0.1.20 -a 'use-a-long-generated-password' --no-auth-warning ping

Never bind Redis to a public interface without network restrictions.

6. Managed Redis and TLS

Managed services commonly require TLS. A redis:// URL to a TLS-only endpoint fails, often with connection resets or timeouts rather than a clear message. Use rediss://:

REDIS_URL=rediss://default:password@my-redis.example-provider.com:6380/0

Celery with TLS Redis may also need SSL options for the broker and backend, such as certificate requirements:

import ssl

CELERY_BROKER_USE_SSL = {"ssl_cert_reqs": ssl.CERT_REQUIRED}
CELERY_REDIS_BACKEND_USE_SSL = {"ssl_cert_reqs": ssl.CERT_REQUIRED}

Check your provider's documentation for the port and certificate authority.

7. Connection limits and resource problems

If errors are intermittent and Redis is running:

redis-cli INFO clients | grep -E "connected_clients|blocked_clients"
redis-cli CONFIG GET maxclients

Hitting maxclients produces a different error (max number of clients reached), but a Redis instance that is restarting because of memory pressure produces brief connection refused errors. Check for OOM kills of redis-server in the kernel log.

8. Make Django resilient to Redis outages

Decide what should happen when Redis is unavailable:

  • Cache: wrap non-critical cache access so a failure falls back to computing the value.
  • Sessions: SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" keeps sessions in the database, so a cache outage does not log users out.
  • Celery submission: a broker outage makes .delay() raise after retries; record critical work in the database first and enqueue from a retry loop or outbox pattern.
decide what a Redis outage meansOn outageSettingCache✓treat as a missdjango-redis IGNORE_EXCEPTIONSSessions✓fall back to the DBcached_db engineCelery broker!retry publishingbroker_connection_retryA cache outage should slow the site, not take it downdjango-deployment.com
Short socket timeouts keep a dead Redis from hanging requests.

Set short socket timeouts so requests fail fast instead of hanging:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": os.environ["REDIS_URL"],
        "OPTIONS": {"socket_connect_timeout": 2, "socket_timeout": 2},
    }
}

9. Verify

redis-cli -u "$REDIS_URL" ping
python manage.py shell -c "from django.core.cache import cache; cache.set('redis-check', 1, 10); print(cache.get('redis-check'))"
celery -A config inspect ping

Verification check:

  • PONG from the app host or container
  • the Django cache round-trip prints 1
  • Celery workers reply to inspect ping
  • Redis is not reachable from outside the private network

Explanation

"Connection refused" means the TCP handshake reached a host that actively rejected it, because no process listens on that address and port. With Redis, that is almost always one of three situations: Redis is down, Redis listens only on loopback while the client connects over the network, or the client targets the wrong host because of container networking. Checking the listening socket on the server and testing the exact URL from the client's own environment separates those cases in a couple of commands.

Edge cases / notes

IPv6 localhost

A URL with localhost may resolve to ::1 first. If Redis listens only on 127.0.0.1, IPv6 attempts are refused. Use 127.0.0.1 explicitly or bind both.

Unix sockets

For Redis on the same host, a Unix socket avoids TCP entirely: unix:///run/redis/redis-server.sock?db=0. The app user must have permission on the socket, typically through the redis group.

Sentinel and cluster

Redis Sentinel and Redis Cluster need client support and different URLs or options. A plain redis:// URL pointing at a Sentinel port results in protocol errors, not normal operation.

A connectivity checklist script

When an incident starts, running the same checks every time saves minutes. This script tests DNS, TCP, and the Redis protocol for a URL, from the machine or container where it runs:

# scripts/check_redis.py
import os
import socket
import sys
from urllib.parse import urlparse

import redis

url = os.environ.get("REDIS_URL") or sys.argv[1]
parsed = urlparse(url)
host, port = parsed.hostname, parsed.port or 6379

try:
    addresses = {info[4][0] for info in socket.getaddrinfo(host, port)}
    print(f"DNS ok: {host} -> {', '.join(sorted(addresses))}")
except socket.gaierror as exc:
    sys.exit(f"DNS failed for {host}: {exc}")

try:
    socket.create_connection((host, port), timeout=3).close()
    print(f"TCP ok: {host}:{port}")
except OSError as exc:
    sys.exit(f"TCP failed for {host}:{port}: {exc}")

try:
    client = redis.Redis.from_url(url, socket_connect_timeout=3, socket_timeout=3)
    print("PING:", client.ping())
    print("server version:", client.info("server").get("redis_version"))
except redis.RedisError as exc:
    sys.exit(f"Redis protocol or auth failed: {exc}")

Run it inside the web container, the worker container, and on the host. Different results between them point directly at a networking or configuration difference. The script prints no credentials.

Health checks for Redis in Compose

  redis:
    image: redis:7
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" --no-auth-warning ping | grep PONG"]
      interval: 5s
      timeout: 3s
      retries: 10
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}

Combine it with depends_on: redis: condition: service_healthy on services that connect at startup, such as Celery workers.

Common mistakes

  • Using 127.0.0.1 for Redis inside Docker containers.
  • Changing bind to 0.0.0.0 on a public server without a firewall.
  • Updating the web environment but not the Celery worker's.
  • Using redis:// for a TLS-only managed endpoint.

Next steps

FAQ

What does "Error 111 connecting to 127.0.0.1:6379. Connection refused" mean?

Nothing is listening on port 6379 at that address from the client's point of view. Redis may be stopped, bound to another interface, or running in a different container or host.

Why can't my Django container connect to Redis on localhost?

Inside a container, localhost is the container itself. Use the Docker Compose service name, such as redis, as the hostname.

How do I allow remote connections to Redis for Django?

Bind Redis to the private network interface, set a password or ACL user, and allow port 6379 only from your application servers in the firewall.

Do I need rediss:// for managed Redis?

Use rediss:// whenever the provider requires TLS, which most managed services do. Plain redis:// connections to TLS endpoints fail.

Keep going