Fix Heroku R14 "Memory Quota Exceeded" for a Django App
Fix Heroku R14 Memory quota exceeded and R15 errors for Django: measure memory per Gunicorn worker, set WEB_CONCURRENCY to fit the dyno, recycle workers with max-requests, find leaks in views and Celery, and choose a dyno size that matches.
Problem statement
Heroku logs fill with:
heroku[web.1]: Process running mem=612M(119.6%)
heroku[web.1]: Error R14 (Memory quota exceeded)
Response times climb, because once a dyno passes its memory quota it starts swapping to disk. If usage reaches twice the quota, Heroku kills the dyno with R15 (Memory quota vastly exceeded), and users see H10 or H13 errors during the restart. A Django dyno runs several Gunicorn worker processes, each a full copy of your app, so total memory is roughly workers times memory per worker, plus growth over time.
- Check the dyno size and memory:
heroku psand the Metrics tab (memory usage, swap). - Set worker count to what fits:
heroku config:set WEB_CONCURRENCY=2on a 512 MB dyno. - Recycle workers: add
--max-requests 1000 --max-requests-jitter 100to Gunicorn. - Find memory-heavy views: large querysets loaded at once, big file uploads in memory, unbounded caches.
- Move Celery or other processes to their own dynos, or step up the dyno size.
Step-by-step solution
1. Measure memory per worker
Enable log-runtime-metrics or watch the Metrics tab. Then estimate per-worker memory with one worker:
heroku config:set WEB_CONCURRENCY=1
heroku logs --tail | grep "mem="
A typical Django app uses 120–250 MB per worker after warming up. Divide the dyno quota by that number and leave headroom: a 512 MB dyno fits two workers of 180 MB, not four.
2. Set WEB_CONCURRENCY explicitly
The Heroku Python buildpack sets a default WEB_CONCURRENCY based on dyno size, and Gunicorn reads it. Defaults assume a small app; set it from measurement:
heroku config:set WEB_CONCURRENCY=2
# Procfile
web: gunicorn config.wsgi --max-requests 1000 --max-requests-jitter 100 --timeout 25
If you need more concurrency for I/O-bound views, add threads (--threads 4) instead of processes. Threads share memory.
3. Recycle workers to contain growth
Python processes rarely return memory to the OS, and some libraries leak slowly. --max-requests restarts each worker after a number of requests, and jitter spreads the restarts:
web: gunicorn config.wsgi --max-requests 1000 --max-requests-jitter 100
If memory keeps rising between restarts, find the leak rather than lowering the number further.
4. Find memory-hungry code
Common causes in Django:
- Iterating over a large queryset loads every row into memory. Use
.iterator(chunk_size=2000)or paginate. - Building a large CSV or JSON response in memory. Stream it with
StreamingHttpResponse. FILE_UPLOAD_MAX_MEMORY_SIZEset high, keeping uploads in RAM. Let large files spool to disk.- Module-level caches (dicts that grow per request) or
lru_cachewithout a size limit. DEBUG = Truein production, which stores every SQL query in memory.
5. Separate process types
A Procfile that starts Celery inside the web dyno (with honcho or &) makes both share one quota. Give each its own process type:
web: gunicorn config.wsgi --max-requests 1000 --max-requests-jitter 100
worker: celery -A config worker --concurrency 2 --max-tasks-per-child 200
Celery's --max-tasks-per-child plays the same role as Gunicorn's --max-requests.
Explanation
Each Heroku dyno has a fixed memory quota by size. Gunicorn's pre-fork model creates independent processes, each importing Django, your apps, and their dependencies, so memory multiplies with worker count. On top of that baseline, each request allocates objects; CPython's allocator keeps freed memory for reuse, so a worker's resident size tends to stay at its high-water mark. R14 is Heroku's warning that the dyno passed its quota and started swapping; R15 is the hard limit.
Edge cases / notes
- preload_app.
--preloadloads the app before forking, sharing some memory between workers through copy-on-write. Gains are modest in CPython because reference counting touches shared pages. - Pillow and NumPy. Image processing and data libraries can allocate hundreds of megabytes per request. Offload heavy work to worker dynos.
- Performance dynos. Larger dynos have more memory per unit cost, which can be cheaper than many small ones for memory-heavy apps.
Next steps
- For the crashes that follow R15, see Fix Heroku H10 App Crashed for Django.
- Track down leaks with Fix Django Memory Leak in Production.
- Choose worker counts with Optimize Gunicorn Workers for Django.
FAQ
Does R14 crash my app?
Not immediately. The dyno swaps to disk and slows down. At double the quota, R15 kills it.
How many Gunicorn workers fit on a basic dyno?
Usually two on a 512 MB dyno for a typical Django app. Measure your per-worker memory to be sure.
Will max-requests hide a memory leak?
It contains the damage by restarting workers periodically. Find the leak if memory still climbs quickly.
Should I upgrade the dyno instead?
If per-worker memory is reasonable and you need more concurrency, yes. If memory grows without bound, a bigger dyno only delays R14.