Gunicorn kept restarting. The culprit was 8MB of JSON.

๐Ÿ’ป Code Chaos 2026-07-05 ยท Updated 2026-10-18 2 min read

Flask behind Gunicorn: four workers restart silently every 5-10 minutes with no traceback. An 8MB JSON file was re-parsed on every request in before_request.

💡 What You Will Learn

Flask behind Gunicorn: four workers restart silently every 5-10 minutes with no traceback. An 8MB JSON file was re-parsed on every request in before_request.

📜 Table of Contents

Gunicorn kept restarting. The culprit was 8MB of JSON.

No 500s, no traceback, only a few worker-exit lines. Monitoring showed RSS climbing, then dropping. The clue was the memory curve.

Who kills the worker

Symptom Cause Confirm with
WORKER TIMEOUT Request exceeded the worker timeout --log-level debug
Worker exiting (pid) Hit --max-requests Check start flags
Killed process ... (python) OOM killer dmesg -T \| grep -i oom

Here the platform supervisor killed workers when RSS crossed a threshold, so nothing reached the app log.

Four commands

ps -eo pid,rss,etime,cmd --sort=-rss | grep gunicorn | head
watch -n 5 'ps -eo pid,rss,cmd | grep "[g]unicorn"'
dmesg -T | grep -i -E "oom|killed process"
gunicorn app:app --log-level debug --access-logfile - --error-logfile -

All four workers climbing together means per-request allocation that never returns.

Root cause

@app.before_request
def load_config():
    with open("/srv/app/config.json") as f:   # 8MB
        g.config = json.load(f)
    g.index = build_index(g.config)

before_request runs on every request, including health checks. An 8MB JSON expands 5-20x in Python objects, plus a full copy for the index. Peak memory rises and the allocator does not return it to the OS.

Fix

def _load_config():
    with open("/srv/app/config.json", "rb") as f:
        return json.load(f)

CONFIG = _load_config()
INDEX = build_index(CONFIG)

Open in binary mode, build indexes at startup so lookups are O(1), and remember each worker holds its own copy โ€” the default is (2 ร— CPUs) + 1 workers.

gunicorn app:app --preload --workers 4
gunicorn app:app --workers 4 --max-requests 2000 --max-requests-jitter 200

--preload shares read-only pages via copy-on-write; move DB connections and threads into post_fork. --max-requests is a tourniquet, not a fix.

Verify

Load-test for five minutes with wrk while watching RSS. A flat curve is the proof, along with zero restarts over 24 hours.

FAQ

Why no errors? The process was killed externally, by the platform or the kernel.

What value for --max-requests? No universal number; it admits a leak exists.

Only fails in production? Local runs are single-worker, low traffic, short-lived.

Gunicorn defaults: see official docs. Supervisor thresholds vary.

Related

Related Articles
2026-07-04
My horse optimized a pencil. Added Bluetooth.
2026-06-22
My horse helped a kid with homework. Kid got an A.
2026-06-29
My horse heard write a poem and wrote a Python script
2026-06-29
My horse spent 2 hours saving 1 minute
2026-07-11
My AI wrote a bug, then fixed it itself โ€” the self-healing moment
2026-06-28
My horse created a password so secure even it couldn't recover it

Written by our editorial team; tools listed here are tested or verified against public sources. Links point to official sites or GitHub repos for reference only โ€” no paid placements.

๐Ÿ’ฌ Comments (0)

No comments yet. Be the first!

Login to comment