Python Deadlock Debugging: faulthandler Plus gdb for Wedged Workers

Python Deadlock Debugging: faulthandler Plus gdb for Wedged Workers
19 views
When a worker stops answering health checks but CPU is idle, you are usually in a lock graph—not a “slow query.” The unfair advantage is dumping all Python threads with faulthandler and, when that is silent, attaching gdb for native frames inside C extensions.
⚡ TL;DR: Enable faulthandler dump-on-signal and periodic dumps; on wedge send SIGUSR1/SIGABRT per policy; if stacks show native wait, use gdb + py-bt; fix lock ordering across Python and extension locks. Pair with Distributed Deadlock Detection and Python Prefork Pitfalls.

Python Deadlock Debugging: production guidance

python
# ✅ boot path in gunicorn/uvicorn worker
import faulthandler
import signal
import sys

faulthandler.enable(file=sys.stderr, all_threads=True)
# Dump all threads on SIGUSR1 without killing the process
faulthandler.register(signal.SIGUSR1, file=sys.stderr, all_threads=True)
# Optional: periodic dump every 5 minutes while debugging a fleet
# faulthandler.dump_traceback_later(300, repeat=True, file=sys.stderr)
bash
# From the host / sidecar when readiness fails
kill -USR1 $(pgrep -f "uvicorn.*worker")
# stderr now has every Python thread stack

When Python stacks lie

If every thread is in PyEval_RestoreThread / extension pthread_mutex_lock, you need native frames.
bash
# ✅ gdb against the wedged PID (ECS exec / privileged debug sidecar)
gdb -p "$PID" -batch \
 -ex "set pagination off" \
 -ex "thread apply all bt" \
 -ex "thread apply all py-bt" \
 -ex "detach" -ex "quit"
python
# Reproduce classic extension + GIL hazard in staging
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()

def t1():
 with lock_a:
 with lock_b:
 pass

def t2():
 with lock_b:
 with lock_a: # ❌ inverted order → deadlock under race
 pass
Signal / tool Shows Kills process?
SIGUSR1 + faulthandler Python stacks No
SIGABRT + faulthandler Python stacks + abort Yes
gdb py-bt Python + C No (attach)
py-spy dump Python samples No

Production playbook

  1. Confirm wedge: readiness fail, no RPS, CPU ~0, FDs stable.
  2. kill -USR1 → collect stacks to CloudWatch / file.
  3. If stuck in native code → ephemeral debug sidecar with matching symbols.
  4. Map lock graph; enforce global order; add timeouts (lock.acquire(timeout=…)).
  5. Add a canary that holds inverted locks in CI so the bug fails before prod.
❌ Restarting the task as the only remediation — you destroy evidence and train the org to ignore systemic lock bugs.

Closing checklist

✅ Dos – ✅ Register faulthandler on worker boot – ✅ Document which signal dumps vs kills – ✅ Keep debug images with CPython debug symbols available – ✅ Enforce lock ordering docs in CODEOWNERS-owned modules – ✅ Prefer timeouts on cross-component locks ❌ Don’ts – ❌ Don’t ship without a dump signal in production – ❌ Don’t attach gdb to every task by default (audit + ephemeral only) – ❌ Don’t ignore wedges that “clear after bounce” – ❌ Don’t mix multiprocessing locks with threads without a design

Related reading

Last updated on September 11, 2026 Related: Python tracemalloc in Workers: Find Retained Caches Without Noise; Python Lambda Warm Strategies: Lean Imports Plus Provisioned Concurrency; Python’s slots Secret: Cut Memory 60% and Speed Up Attribute Access by 35%.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.