hello@logkit.io · Status · GitHub
AC

By Alex Chen

Senior Backend Engineer at LogKit

How We Debugged a Cascading Failure Across 12 Microservices in 11 Minutes

Incident Postmortem Microservices LogKit Oct 24, 2023

A real-world look at how structured logging and context propagation turned a production crisis into a 15-minute debugging sprint.

Microservices architecture diagram showing 12 interconnected services

The Scene: 2:14 AM

It started with a single PagerDuty alert: [ALERT] High Error Rate detected in Production Cluster. The flag was raised on the Order Service at 2:14 AM EST.

The symptoms were vague. The error logs were flooded with 503 Service Unavailable responses. The on-call engineer, myself, opened the terminal to SSH into the primary instance.

We knew immediately: 12 services were implicated. The Order Service, Inventory Sync, Payment Gateway, Notification Service, and seven others were all reporting errors simultaneously. Was it a database connection pool exhaustion? A DNS failure? Or something more insidious?

The Old Way: Greps and Grief

In the pre-LogKit era, this would have been a nightmare. I would have had to SSH into the three most critical nodes—Order, Inventory, and Payment—run tail -f /var/log/app.log, and frantically scroll for a common thread.

The logs were inconsistent. The auth-api was dumping JSON, but the legacy legacy-worker was dumping raw strings. Most critically, the trace_id was missing from the legacy worker logs entirely.

I spent twenty minutes grepping for timestamps, cross-referencing server IPs, and trying to manually map errors back to user sessions. By the time I found the first correlation ID in the Payment logs, the alert window had closed, and the logs had already rotated.

The LogKit Way: One Trace, Infinite Clarity

Today, the workflow is completely different. I didn't SSH into any servers. I opened the LogKit dashboard, navigated to the Trace Explorer, and pasted the Trace ID provided by the alert.

Within seconds, the platform surfaced the entire execution graph of the failed request. I saw the request flow: Order ServiceInventory SyncPayment Gateway.

The time from alert to diagnosis? 11 minutes. Not 45. Not 2 hours.

Step-by-Step: The Debugging Session

Here is how the query revealed the truth in real-time:

LogKit Query: trace_7f9a2b
# Filter by Trace ID
where trace_id == "7f9a2b"

# Order Service Error
{
  "service": "order",
  "level": "error",
  "error_code": "500",
  "message": "payment_timeout"
}

# Payment Gateway Response
{
  "service": "payment",
  "level": "warn",
  "error_code": "503",
  "message": "retry_budget_exceeded"
}

Root Cause Revealed

The query showed that the Order Service was trying to sync inventory, but the Inventory Sync service was timing out. Inventory was waiting on the Payment Gateway.

The Payment Gateway was rejecting requests with 503 and a specific warning: retry_budget_exceeded.

We dug into the Payment Service configuration. A recent deployment (PR #402) had lowered the max_retries setting from 5 to 0 for the Stripe adapter.

Why? The developer had assumed the external Stripe gateway was the bottleneck, so they disabled retries locally. They were wrong. The bottleneck was the Inventory Sync service, which was slower than expected. By removing the retry budget, the Payment service was failing fast and cascading failures to the Order service.

Takeaways for Log Design

This incident highlighted three non-negotiable requirements for modern logging:

  • Context Propagation: Every log line must carry the trace_id. Without it, distributed tracing is impossible.
  • Schema Consistency: If the Order service expects a JSON payload and the Payment service sends a string, you can't query them together. LogKit enforces a strict schema at the SDK level.
  • Structured Fields: Don't log "Payment failed". Log "error_code": "503", "service": "payment", "retry_budget": 0. This allows for intelligent alerting and filtering.

About the Author

Alex Chen is a Senior Backend Engineer at LogKit with 10 years of experience building distributed systems. He specializes in Go, Kubernetes, and observability strategies.

View all posts by Alex →

Join the discussion

Did you encounter a similar cascading failure? How do you handle context propagation in your stack?

Leave a comment
Related Reading

More from the Engineering Blog

Zero-Allocation Logging in Go

How we optimized the LogKit SDK to run with zero GC pauses on the hot path.

Read more →

Designing for Observability

A guide to designing APIs and services that make debugging easier for your team.

Read more →

The State of Structured Logs

Why JSON is winning over text formats and what it means for your data warehouse.

Read more →