DEV Community

Sir Max
Sir Max

Posted on

How I Cut Debugging Time in Half: 4 Practices That Actually Work

How I Cut Debugging Time in Half: 4 Practices That Actually Work

I used to spend hours chasing bugs that, in hindsight, should have taken 15 minutes. You know the pattern: stare at code, add print statements, refresh, stare more, eventually fix the wrong thing, repeat.

After shipping production code for a few years and debugging over 100 incidents, I've found four practices that consistently cut my debugging time. Not "productivity hacks" — just actual habits that changed how I approach broken code.


1. Structured Logging Before You Need It

The worst time to realize your logs are useless is at 2 AM during an incident.

I learned this the hard way. We had a payment processing service that would occasionally fail with a generic "transaction error." The logs? Just the error message. No request ID, no payload, no upstream response.

What I do now: Every HTTP request gets a unique request_id (UUID) that propagates through all downstream calls:

import uuid
import logging
from contextvars import ContextVar

request_id_var: ContextVar[str] = ContextVar("request_id")

class RequestIdFilter(logging.Filter):
    def filter(self, record):
        try:
            record.request_id = request_id_var.get()
        except LookupError:
            record.request_id = "no-request-id"
        return True

def log_incoming(func):
    async def wrapper(request, *args, **kwargs):
        rid = str(uuid.uuid4())
        request_id_var.set(rid)
        logger.info("request_start", extra={"method": request.method, "path": request.url.path})
        try:
            response = await func(request, *args, **kwargs)
            logger.info("request_end", extra={"status": response.status_code})
            return response
        except Exception:
            logger.exception("request_error")
            raise
    return wrapper
Enter fullscreen mode Exit fullscreen mode

Use structured logging (JSON), not formatted strings. You can query request_id=abc123 and see the entire lifecycle of that one request — from gateway through service to database and back.


2. Reproduce First, Fix Second

This sounds obvious. But I've watched developers (including past me) jump straight into code changes based on a vague error description. "The search page is slow" → start adding indexes. Wrong move.

The rule: If you can't reproduce it, you don't understand it.

My workflow now:

  1. Capture the exact input — request payload, user session, timing. Screenshots aren't enough.
  2. Reproduce locally with that input — if you can't, the bug isn't what you think it is.
  3. Write a failing test — this is your proof you understand the problem.

I once spent two hours "fixing" a race condition that didn't exist. The real problem was a misconfigured load balancer timeout. If I'd actually reproduced the issue — sent the same request payload and watched the full trace — I would have spotted it in 10 minutes.

Hard rule: no code change until I've reproduced the failure with my own eyes.


3. Binary Search Debugging

When the bug could be anywhere in a complex system, don't guess. Bisect.

The technique is the same as git bisect, but applied to the runtime pipeline:

  • Check the middle: Is it still broken if you skip half the steps?
  • Narrow down: Which half contains the problem?
  • Repeat: Until you're down to one function or one configuration change.

Example from a real incident: Users reported intermittent 500 errors on a dashboard page. The pipeline was: Browser → CDN → API Gateway → Auth Service → Dashboard Service → Database.

Instead of checking each layer sequentially, I started in the middle:

  1. Hit the Dashboard Service directly (bypass gateway + auth) → still failing. Right half (Auth→DB) is ruled out.
  2. Check the gateway logs → errors there too. Left half contains the issue.
  3. Compare CDN vs direct-to-gateway → CDN was fine. Gateway was the culprit.
  4. Gateway config diff → someone had changed the timeout from 30s to 5s a day earlier.

Twenty minutes total. Sequential debugging would have taken hours.


4. The Debug Journal

I keep a plain text file open during any non-trivial debugging session. Every observation goes in:

14:05 - user reports "page not loading" with screenshot
14:08 - reproduced on staging. 502 from upstream
14:12 - upstream service logs show OOMKilled
14:15 - memory graph shows spike at 13:45 — same time as batch job
14:20 - confirmed: batch job memory leak, fixed in v2.1 but not deployed
14:30 - deployed v2.1, monitoring for 5 min, stable
Enter fullscreen mode Exit fullscreen mode

Why this works:

  • Prevents looping — if you've already tried something, the journal shows it. No repeating dead ends.
  • Captures context — 3 hours later, you won't remember the exact error message. The journal does.
  • Makes handoffs easy — paste the journal into the incident ticket and someone else can pick it up.
  • Builds pattern recognition — after 50 entries, you start seeing recurring failure modes.

It sounds too simple to matter. But after 100+ incidents, my journal has caught patterns I wouldn't have noticed otherwise: "third time this month the Redis connection pool exhausts under batch load" — that's not a bug, that's a design problem.


Putting It Together

None of these are revolutionary. They just require doing them consistently:

Practice Before (time wasted) After
Structured logging Hours searching scattered logs 30 seconds with request ID
Reproduce first 2h fixing wrong thing 10 min finding real cause
Binary search 1h checking every layer 15 min of bisecting
Debug journal Repeating dead ends Never loops, builds patterns

The hardest one for me was "reproduce first." There's a strong urge to just do something — change code, restart a service, clear a cache. Resist it. Understanding must come before action.

What debugging habits made the biggest difference for you? I'm always looking for new ones.

Top comments (0)