Fix Redis SSL CERTIFICATE_VERIFY_FAILED With rediss:// in Django and Celery

Fix SSL CERTIFICATE_VERIFY_FAILED errors when Django, Celery, or Channels connect to Redis over rediss:// on Heroku, Upstash, ElastiCache, or Azure: configure ssl_cert_reqs, CA bundles, and broker_use_ssl without disabling verification blindly.

Problem statement

After switching Redis to TLS, or moving to a provider that requires it, connections fail:

redis.exceptions.ConnectionError: Error 1 connecting to example.redis.host:6380.
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate
in certificate chain

Celery may refuse to start with a different message:

ValueError: A rediss:// URL must have parameter ssl_cert_reqs and this must be set
to CERT_REQUIRED, CERT_OPTIONAL, or CERT_NONE

The client is verifying the server certificate, as it should, and cannot build a chain to a trusted certificate authority. Some providers, notably Heroku Redis, use self-signed certificates. Others use public CAs but your container lacks CA certificates, or the host name you connect to does not match the certificate.

Why TLS verification failsrediss://TLS requestedServer certself-signed or privateClient CA storecannot verifyHandshake failsVERIFY_FAILEDGive the client the right CA, or relax verification only where the provider requires it.
  1. Find out how your provider signs certificates: public CA (Upstash, ElastiCache, Azure) or self-signed (Heroku Redis).
  2. Public CA: install ca-certificates in the image and connect using the provider's host name.
  3. Self-signed with a provided CA file: pass it with ssl_ca_certs.
  4. Heroku Redis: set ssl_cert_reqs=none explicitly for every client, since Heroku documents this for its self-signed certificates.
  5. For Celery, add ?ssl_cert_reqs=CERT_REQUIRED (or CERT_NONE) to rediss:// URLs, or set broker_use_ssl.

Step-by-step solution

1. Inspect the certificate

openssl s_client -connect example.redis.host:6380 -servername example.redis.host </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

An issuer such as Amazon or Let's Encrypt means a public CA. An issuer equal to the subject, or a private CA name, means you need the provider's CA file or must disable verification.

2. Public CA: fix the trust store

Slim images may lack CA certificates:

RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*

Also connect with the host name on the certificate, not an IP address or an internal alias, or verification fails on the name check even with a valid chain.

3. Configure Django's cache

With django-redis:

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": os.environ["REDIS_URL"],  # rediss://...
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": "required"},
        },
    }
}

With a private CA, add "ssl_ca_certs": "/etc/ssl/certs/provider-ca.pem". For Heroku Redis, use "ssl_cert_reqs": None as Heroku's documentation describes. Django's built-in Redis backend accepts the same keys under "OPTIONS".

what each provider needsCertificatessl_cert_reqsUpstashpublic CArequiredElastiCachepublic CArequiredAzure Cachepublic CArequiredHeroku Redis!self-signednone (documented)Keep verification on wherever the provider uses a public CAdjango-deployment.com
Check your provider’s current documentation, since certificate practices change.

4. Configure Celery

Celery requires an explicit choice for rediss:// URLs. Either in the URL:

CELERY_BROKER_URL = os.environ["REDIS_URL"] + "?ssl_cert_reqs=CERT_REQUIRED"
CELERY_RESULT_BACKEND = CELERY_BROKER_URL

or in settings:

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

Use CERT_NONE only for providers like Heroku Redis that require it. If your REDIS_URL already contains a query string, append with & instead of ?.

5. Configure Channels

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {"hosts": [{"address": os.environ["REDIS_URL"], "ssl_cert_reqs": None}]},
    }
}

Drop ssl_cert_reqs when the provider uses a public CA.

configure each oneDjango cacheCONNECTION_POOL_KWARGSCelery brokerbroker_use_ssl or URLCelery result backendredis_backend_use_sslChannels layerhost dict optionsSessions, RQ, rate limitstheir own clientsFixing one client leaves the others failingdjango-deployment.com
Search settings for every rediss:// use before declaring the fix complete.

Explanation

redis-py wraps the socket in TLS when the URL scheme is rediss:// and, since redis-py 4, verifies the certificate by default using the system CA store. Verification fails when the chain ends in a certificate the store does not trust or when the host name does not match. Celery adds its own check because a rediss:// URL without an explicit choice was historically ambiguous. Disabling verification still encrypts traffic but no longer proves you are talking to your Redis; it is acceptable only where the provider gives no CA to verify against.

Edge cases / notes

  • Heroku certificate changes. Heroku Redis has changed TLS behavior over time; check their current guidance for the plan you use.
  • Corporate proxies. TLS-inspecting proxies replace certificates; add the proxy CA to the trust store rather than disabling verification.
  • Old redis-py. Versions before 4 did not verify by default, so an upgrade can turn a silent setup into this error.

Next steps

FAQ

Is ssl_cert_reqs=none insecure?

It keeps encryption but skips identity verification, so a network attacker could impersonate the server. Use it only when the provider requires it.

Why does Celery demand ssl_cert_reqs in the URL?

Celery requires an explicit verification choice for rediss:// URLs so the security behavior is never accidental.

Do I need TLS on a private network?

Many managed providers require it. Where optional, it still protects credentials and data from other hosts on the network.

Why does it work locally but fail in Docker?

The slim image probably lacks ca-certificates, so even public CAs cannot be verified.

Keep going