Fix Django Cache Errors After Deploy: "Can’t Get Attribute" When Unpickling
Fix Django errors after a deploy such as AttributeError Can’t get attribute on module, ModuleNotFoundError, or TypeError when reading cached objects: version cache keys per release, avoid pickling model instances, and clear or namespace the cache safely.
Problem statement
Right after a deploy, some pages fail with tracebacks from the cache layer:
AttributeError: Can't get attribute 'PriceSummary' on <module 'shop.services' ...>
ModuleNotFoundError: No module named 'shop.utils.pricing'
TypeError: __init__() missing 1 required positional argument: 'currency'
The errors fade as cache entries expire. Django's Redis and Memcached backends store Python objects with pickle. A pickle records the class's module path and name, and when read back, Python imports that module and rebuilds the object. If the new release renamed, moved, or changed the class, objects cached by the old release cannot be rebuilt. During a rolling deploy, old and new code even read each other's entries at the same time.
- Short term: clear the affected keys, or the whole cache if it holds only disposable data.
- Version cache keys per release: set
KEY_PREFIXorVERSIONfrom the release identifier. - Cache plain data (dicts, lists, strings, numbers) instead of instances of your own classes.
- Catch errors on cache reads and treat them as misses.
- Keep sessions out of a cache that you flush on deploys.
Step-by-step solution
1. Recover now
If the cache holds only data that can be recomputed:
python manage.py shell -c "from django.core.cache import cache; cache.clear()"
cache.clear() removes every key in that cache's Redis database, including sessions if you use the cache session backend, which logs everyone out. If sessions share the database, delete by pattern instead with django-redis: cache.delete_pattern("shop:*").
2. Namespace keys by release
Give every release its own key space so old and new code never read each other's entries:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": os.environ["REDIS_URL"],
"KEY_PREFIX": "shop",
"VERSION": os.environ.get("CACHE_VERSION", "1"),
}
}
Set CACHE_VERSION to the commit SHA in your deploy. Old entries are ignored and expire on their own. The cost is a cold cache after every deploy, so do this for caches of computed objects, not for expensive data that rarely changes shape.
3. Cache plain data
Instead of caching a PriceSummary instance, cache what it is made of:
def get_price_summary(product_id):
key = f"price-summary:{product_id}"
data = cache.get(key)
if data is None:
summary = compute_price_summary(product_id)
data = {"net": str(summary.net), "tax": str(summary.tax), "currency": summary.currency}
cache.set(key, data, 300)
return PriceSummary.from_dict(data)
Model instances are a special case: Django pickles them with a version marker and warns about mismatches, but a changed model can still fail to load. Cache primary keys or values() output rather than instances.
4. Treat unreadable entries as misses
Wrap reads so a bad entry falls back to recomputing:
import logging, pickle
logger = logging.getLogger(__name__)
def safe_get(key, default=None):
try:
return cache.get(key, default)
except (AttributeError, ModuleNotFoundError, ImportError, TypeError, pickle.UnpicklingError):
logger.warning("discarding unreadable cache entry %s", key)
cache.delete(key)
return default
5. Mind rolling deploys
During a rolling or blue-green deploy, old pods keep writing entries the new pods read, and the reverse. Versioned keys solve this cleanly. Without them, renames must be done in two releases: first add the new class while keeping the old name importable, then remove the old one after the cache has turned over.
Explanation
pickle serializes an object as instructions that name its class by import path, plus its state. Loading runs those instructions, importing the module and looking up the class by name. Moving PriceSummary from shop.utils.pricing to shop.services breaks every existing pickle that names the old path, and changing __init__ or __slots__ can break reconstruction even without a move. Django's cache backends use pickle by default for convenience, which ties cache contents to the code that wrote them.
Edge cases / notes
- JSON serializer. django-redis supports a JSON serializer, which forces plain data and avoids this class of error entirely, at the cost of not caching arbitrary objects.
- Celery results. Celery's result backend can hit the same problem with pickle; use the JSON serializer for tasks and results.
- Sessions. Django sessions use JSON by default, so sessions are not affected unless you switched to
PickleSerializer.
Next steps
- Design what to cache with Django Caching Strategy for Production.
- Set up Redis properly with Set Up Redis Cache for Django in Production.
- For workers running old code after deploys, see Fix Celery Tasks Running Old Code After Deploy.
FAQ
Why do the errors disappear after a while?
Old entries expire based on their timeout, and new ones are written by the new code.
Is clearing the cache on every deploy a good idea?
It works for small sites but causes a load spike on busy ones. Versioned keys achieve the same isolation and let old entries expire quietly.
Can I cache model instances safely?
It works until the model changes. Caching primary keys or values() dictionaries is more robust.
Does this affect Memcached too?
Yes. Django's Memcached backend also pickles values.