| name | debugging-production |
| description | Debug production incidents and hard-to-reproduce bugs — use when someone asks how to diagnose a JVM memory leak, Kubernetes crash, slow query, production exception, or needs to read thread dumps, heap dumps, or flamegraphs. |
Debugging Production Systems
Debugging Mindset
Hypothesis-Driven Debugging
Observation: service response time increased 10x at 14:32 UTC
|
v
Hypothesis 1: traffic spike caused resource exhaustion
|
|--> Experiment: check request rate metrics at 14:32
|--> Result: traffic normal; hypothesis rejected
|
v
Hypothesis 2: a deployment at 14:30 introduced a slow code path
|
|--> Experiment: check deploy history; compare p99 before/after
|--> Result: deploy at 14:28 introduced a new DB query; hypothesis confirmed
|
v
Action: roll back or hotfix the query
Resist the urge to change things randomly hoping something helps. Each change:
- Costs time to deploy and verify
- Can introduce new bugs
- Obscures the root cause if it has a side effect
Reproduce First
A bug you can reproduce consistently is 80% debugged. Approaches:
- Reproduce in a staging environment with production data (anonymized)
- Replay production traffic: capture with
tcpdump, replay with tcpreplay or service-specific tools
- Feature flag: route a small % of traffic to a debug-instrumented version
- Add debug logging to production temporarily (use a log level that can be changed without restart)
If you cannot reproduce, add instrumentation to gather more data. Do not guess.
The Debugging Checklist
Before changing anything:
JVM Production Debugging
Thread Dumps — Identify Deadlocks and Blocked Threads
kill -3 <pid>
kill -QUIT <pid>
jstack <pid>
jstack -l <pid>
jcmd <pid> Thread.print
for i in 1 2 3; do jstack <pid> > /tmp/thread-dump-$i.txt; sleep 30; done
Reading a thread dump:
"http-nio-8080-exec-7" #45 daemon prio=5 os_prio=0 tid=0x00007f RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at com.mysql.cj.protocol.FullReadInputStream.readFully(...)
...
"http-nio-8080-exec-12" #50 daemon prio=5 os_prio=0 tid=0x00007f BLOCKED
waiting to lock <0x00000007b> (a com.example.service.UserCache)
at com.example.service.UserCache.get(UserCache.java:84)
Patterns to look for:
BLOCKED on the same lock object across many threads: contention or deadlock
WAITING on java.util.concurrent.LinkedBlockingQueue.take: thread pool idle (normal)
- All threads
WAITING in socketRead: downstream service not responding
- A thread holding a lock but not releasing it: look for the thread that owns
<0x0000...>
Deadlock detection: jstack will print "Found one Java-level deadlock" and show the cycle.
Heap Dumps — Identify Memory Leaks
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap-dumps/
jcmd <pid> GC.heap_dump /tmp/heap.hprof
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
Analyze with Eclipse Memory Analyzer (MAT):
- Open
heap.hprof in MAT
- Run "Leak Suspects Report" — MAT identifies the largest retained object trees
- Open "Dominator Tree" — find objects retaining the most memory
- Use OQL to query:
SELECT * FROM com.example.service.UserSession WHERE createdAt < ...
Common leak patterns:
- Static collections that grow without eviction (
static Map<UserId, Session>)
- Event listeners not deregistered
- ThreadLocal variables not cleaned up in thread pools
- Cache without size bounds or TTL
Java Flight Recorder — Low-Overhead Continuous Profiling
jcmd <pid> JFR.start duration=60s filename=/tmp/recording.jfr settings=profile
jcmd <pid> JFR.start name=continuous settings=default
jcmd <pid> JFR.dump name=continuous filename=/tmp/recording.jfr
jcmd <pid> JFR.stop name=continuous
jmc
JFR captures: CPU profile, memory allocation profile, GC events, lock contention, I/O waits, network activity — all with minimal overhead.
async-profiler — CPU and Allocation Flamegraph
curl -L https://github.com/async-profiler/async-profiler/releases/latest/download/async-profiler-linux-x64.tar.gz | tar xz
./asprof -d 30 -f /tmp/cpu-flamegraph.html <pid>
./asprof -d 30 -e alloc -f /tmp/alloc-flamegraph.html <pid>
./asprof -d 30 -e wall -t -f /tmp/wall-flamegraph.html <pid>
Reading flamegraphs:
- X-axis: proportion of time spent (wider = more time)
- Y-axis: call stack (bottom = main; top = leaf function)
- Look for wide bars near the top: functions where most time is spent
- Click to zoom; use search to find a specific class or method
GC Analysis
JVM startup flags for GC logging:
-Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=5,filesize=10m
Key GC events to look for:
[info][gc] GC(42) Pause Young (Allocation Failure) ... 250ms <-- high pause; memory pressure
[info][gc] GC(43) Pause Full (Heap Dump Initiated) ... <-- OOM imminent
[info][gc] GC(44) To-space exhausted <-- G1: region allocation failure
Upload gc.log to GCEasy (https://gceasy.io) for visualization of GC activity, throughput, and pause times.
Common GC problems and causes:
Long GC pauses (>500ms) --> heap too small; increase -Xmx; or memory leak
High GC frequency --> too many short-lived objects; check allocation flamegraph
To-space exhausted (G1) --> concurrent marking not keeping up; increase heap or GC threads
Humongous allocation --> single object > region size; allocating large arrays in a loop
Arthas — Hot Diagnostics Without Restart
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar <pid>
watch com.example.service.OrderService createOrder '{params, returnObj}' -x 2
trace com.example.service.OrderService createOrder '#cost > 100'
jad com.example.service.OrderService
redefine /tmp/OrderService.class
classloader -a | grep OrderService
thread -n 5
thread -b
Python Production Debugging
Thread and CPU Profiling
pip install py-spy
py-spy dump --pid <pid>
py-spy record --pid <pid> --output /tmp/profile.svg --duration 30
py-spy top --pid <pid>
faulthandler — Crash Diagnostics
import faulthandler
import signal
faulthandler.enable()
faulthandler.register(signal.SIGUSR2, all_threads=True, chain=False)
kill -USR2 <pid>
manhole — Interactive REPL in Running Process
import manhole
manhole.install()
manhole-cli <pid>
>>> import gc; gc.get_referrers(some_object)
>>> import tracemalloc; tracemalloc.start();
pdb Post-Mortem in Development
try:
process_order(order)
except Exception:
import pdb
pdb.post_mortem()
Memory Profiling
pip install memray
memray attach <pid>
memray flamegraph output.bin
memray run --output output.bin python myscript.py
memray flamegraph output.bin
Kubernetes Debugging
Pod Diagnostics
kubectl describe pod <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous
kubectl logs -f <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --all-containers=true
Interactive Debugging
kubectl exec -it <pod-name> -n <namespace> -- /bin/sh
kubectl exec <pod-name> -n <namespace> -- curl -s localhost:8080/actuator/health
kubectl cp <namespace>/<pod-name>:/tmp/heap.hprof ./heap.hprof
kubectl port-forward svc/<service-name> 8080:80 -n <namespace>
Ephemeral Debug Containers
kubectl debug -it <pod-name> \
--image=nicolaka/netshoot \
--target=<container-name> \
-n <namespace>
curl localhost:8080/health
netstat -tlnp
tcpdump -i eth0 port 8080
dig my-service.my-namespace.svc.cluster.local
Cluster-Wide Event Timeline
kubectl get events --sort-by='.lastTimestamp' -A
kubectl get events -n <namespace> --field-selector type=Warning
kubectl get events -n <namespace> -w
Resource Pressure Debugging
kubectl top nodes
kubectl top pods -n <namespace> --sort-by=memory
kubectl get pod <pod-name> -o jsonpath='{.spec.containers[*].resources}'
kubectl describe pod <pod-name> | grep -A5 "OOMKilled\|Last State"
CrashLoopBackOff Diagnosis
CrashLoopBackOff means: the container started, crashed, and Kubernetes is backing off before restarting it.
Diagnosis sequence:
1. kubectl logs <pod> --previous --> what was the last error?
2. kubectl describe pod <pod> --> what exit code? (OOMKilled=137, segfault=139, app error=1)
3. Check liveness probe config --> probe too aggressive? not enough startup time?
4. Check environment variables / secrets --> missing config causes immediate crash
5. Check init containers --> did init container fail?
Distributed Tracing for Debugging
Finding the Slow Span
Request: POST /api/orders
Total duration: 3.2s <-- 10x normal
Trace breakdown:
[OrderController.createOrder] 50ms
[OrderService.validate] 20ms
[InventoryService HTTP call] 2800ms <-- SLOW SPAN
[HTTP GET /inventory/check] 2800ms
[InventoryController.check] 50ms
[InventoryRepository.findByIds] 2700ms <-- root cause: N+1 query
Action: examine the InventoryRepository.findByIds span — it has 47 child spans, each a separate DB query. Fix: use a batch query.
Finding the Error Span
Request: POST /api/checkout
Result: 500 Internal Server Error
Trace:
[CheckoutController] OK
[CheckoutService] OK
[PaymentService HTTP call] ERROR
status: 422
error: "Card declined: insufficient funds"
<-- This is the root cause; all upstream services returned 500 as a result
Without tracing, you would see a 500 from CheckoutController and have to search logs across 3 services.
Tail-Based Sampling
Problem: sampling at the start of a request means you may not capture the slow or failing requests you care about most.
Tail-based sampling: collect all spans, decide to keep or drop the trace AFTER it completes based on:
- Final status (keep all errors)
- Duration (keep all p99+ latency)
- Random sampling for the rest
OpenTelemetry Collector configuration:
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-traces-policy
type: latency
latency: {threshold_ms: 1000}
- name: probabilistic-policy
type: probabilistic
probabilistic: {sampling_percentage: 5}
Network Debugging
Capture Traffic
tcpdump -i any -w /tmp/capture.pcap port 8080
tcpdump -i any -A port 8080 | grep -A5 'POST\|GET\|HTTP'
kubectl exec -it <pod> -- tcpdump -i eth0 -w /tmp/cap.pcap port 8080
kubectl cp <namespace>/<pod>:/tmp/cap.pcap ./cap.pcap
HTTP Diagnostics
curl -v https://api.example.com/health
curl -w "\n\nDNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nWait: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
-o /dev/null -s https://api.example.com/health
curl -L -v https://api.example.com/orders/123
curl --tlsv1.2 -v https://api.example.com/health
Network Connectivity Checks
ss -tlnp
netstat -tlnp
nc -zv database.internal 5432
dig api.example.com
dig @8.8.8.8 api.example.com
nslookup api.example.com
kubectl exec -it <pod> -- nslookup my-service.my-namespace.svc.cluster.local
kubectl exec -it <pod> -- cat /etc/resolv.conf
Database Debugging
PostgreSQL
SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state
FROM pg_stat_activity
WHERE state != 'idle'
AND (now() - pg_stat_activity.query_start) > interval '1 minute'
ORDER BY duration DESC;
SELECT
blocking.pid AS blocking_pid,
blocking.query AS blocking_query,
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocked.wait_event_type,
blocked.wait_event
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
SELECT pg_terminate_backend(<blocking_pid>);
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE customer_id = 42 AND status = 'PENDING';
Key fields in EXPLAIN ANALYZE output:
Seq Scan on orders (cost=0.00..89432.00 rows=1 width=200)
(actual time=0.042..2341.2 rows=1 loops=1)
Filter: (customer_id = 42)
Rows Removed by Filter: 2341198 <-- scanned 2M rows to find 1: missing index
Buffers: shared hit=0 read=14422 <-- 0 cache hits: cold data or huge table
Auto-explain for slow queries in production:
MySQL / MariaDB
SHOW FULL PROCESSLIST;
KILL QUERY <thread_id>;
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE customer_id = 42;
Connection Pool Exhaustion
Symptom: requests time out; Unable to acquire connection within timeout.
management.endpoint.metrics.enabled=true
SELECT application_name, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;
SHOW max_connections;
Common Production Incident Patterns
Pattern: Sudden Latency Spike
Investigation sequence:
1. Check error rate (errors? or just slow?)
2. Check traffic volume (spike? load shedding needed?)
3. Check downstream services latency (trace shows which span is slow)
4. Check DB: slow queries? lock contention?
5. Check GC: long GC pauses eating into request time?
6. Check deploy history: any deployment 5-30 minutes before the spike?
7. Check CPU/memory: resource saturation?
Pattern: Memory Leak (Heap Growing Until OOM)
Symptoms: memory usage ramps over hours/days; eventually OOM crash
Investigation:
1. Check GC logs: is GC running frequently but unable to free memory?
2. Thread dump: any threads holding references to large objects?
3. Heap dump (when heap is near peak): open in MAT, run Leak Suspects
4. Check dominator tree: which object type is retaining the most memory?
5. Common culprits: unbounded caches, static collections, event listeners,
session objects not expiring, ThreadLocal in thread pools
Pattern: Cascading Failure
Symptoms: multiple services failing; alert storm
Investigation:
1. Identify the "first" failure: check trace/log timestamps across services
2. The first service to fail is the root cause; others are victims
3. Usually: a core dependency (DB, cache, auth service) fails
4. Downstream services retry → amplify load → worsen the failure
Mitigation:
- Circuit breaker opens → stops cascading retries
- Timeout on all downstream calls → fail fast instead of holding threads
- Bulkhead pattern → one failing dependency doesn't exhaust all threads
Pattern: CrashLoopBackOff in Kubernetes
Exit code meanings:
0 --> application exited normally (should not restart unless job)
1 --> application error (check logs)
137 --> OOM killed (SIGKILL; memory limit exceeded)
139 --> segfault (SIGSEGV; JVM crash or native code bug)
143 --> graceful shutdown (SIGTERM)
Action for exit code 137:
1. Increase memory limit temporarily to restore service
2. Take heap dump on next restart (before OOM):
kubectl exec -it <pod> -- jcmd <pid> GC.heap_dump /tmp/heap.hprof
3. kubectl cp to pull the dump
4. Analyze with MAT
Action for exit code 1:
kubectl logs <pod> --previous | tail -100
(find the exception that caused the crash)
Observability Infrastructure for Effective Debugging
Without the right instrumentation, production debugging is guesswork. Ensure:
Structured Logging
{
"timestamp": "2024-01-15T14:32:01.234Z",
"level": "ERROR",
"service": "order-service",
"trace_id": "abc123def456",
"span_id": "789ghi",
"user_id": "user-42",
"order_id": "order-789",
"message": "Payment processing failed",
"error": "Card declined: insufficient funds",
"duration_ms": 234
}
Metrics (RED Method)
- Request Rate: how many requests per second?
- Error Rate: what % are failing?
- Duration: what is the latency distribution (p50, p95, p99)?
Distributed Tracing
- Every external request should have a trace ID
- Propagate trace ID via HTTP headers (
traceparent W3C standard)
- Sample at least 5% of traffic; 100% of errors
Health Endpoints
GET /actuator/health --> liveness: is the process alive?
GET /actuator/health/readiness --> readiness: can it serve traffic?
GET /actuator/metrics --> current metric values
GET /actuator/info --> build version, git commit