Progress · 0/8 sections
- 01 — Load Balancing at Scale (50M Users)
- 02 — Caching Strategies (50M Users, Multi-Layer)
- 03 — Service Discovery & Service Mesh (50M Users
- 04 — Observability in Production (50M Users)
- 05 — Failure Handling & Resilience (50M Users)
- 06 — Distributed Locking & Coordination (50M Use
- 07 — Data Pipelines & Stream Processing (50M Use
- 08 — Deployment & Infrastructure at Scale (50M U
05 — Failure Handling & Resilience (50M Users)
8 min read
05 — Failure Handling & Resilience (50M Users)
Scenario: 50M users, 200K req/sec. Dependencies WILL fail. Networks WILL partition. Servers WILL crash. Question yeh nahi ki "fail hoga kya?" — question yeh hai "jab fail hoga tab system kaise behave karega?"
Core Principle: Design for Failure
Monolith mindset: "Make sure nothing fails"
Distributed mindset: "Everything will fail. Limit the blast radius."
Murphy's Law at scale:
If something can fail at 1% probability per server per day,
with 500 servers, it WILL fail ~5 times daily.
Circuit Breaker Pattern
Problem
Order Service → Payment Service (down)
Without circuit breaker:
Every request tries Payment Service → 5s timeout → fail
200K req/sec × 5s = 1M requests stuck waiting
Thread/goroutine pool exhausted → Order Service ALSO dies
Cascading failure: one service down → everything down
Circuit Breaker States
┌──────────────────────────────────────┐
│ │
▼ │
┌────────┐ failures > threshold ┌────────┐
│ CLOSED │──────────────────────────►│ OPEN │
│(normal) │ │(reject) │
└────────┘ └────┬───┘
▲ │
│ success │ after timeout
│ │
│ ┌────────────┐ │
└────────│ HALF-OPEN │◄──────────────┘
│(test 1 req)│
└────────────┘
│
│ failure → back to OPEN
import "github.com/sony/gobreaker"
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "payment-service",
MaxRequests: 3, // half-open mein 3 test requests
Interval: 30 * time.Second, // counter reset interval (closed state)
Timeout: 10 * time.Second, // open → half-open after 10s
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 10 && failureRatio >= 0.5
// 50% failure rate with at least 10 requests → OPEN
},
OnStateChange: func(name string, from, to gobreaker.State) {
logger.Warn("circuit breaker state change",
zap.String("name", name),
zap.String("from", from.String()),
zap.String("to", to.String()),
)
metrics.CircuitBreakerState.WithLabelValues(name, to.String()).Set(1)
},
})
// Usage
result, err := cb.Execute(func() (interface{}, error) {
return paymentClient.Charge(ctx, amount)
})
if err == gobreaker.ErrOpenState {
// Circuit is open — use fallback
return queuePaymentForLater(ctx, amount)
}Retry with Exponential Backoff + Jitter
❌ Naive Retry = Thundering Herd
1000 requests fail at t=0
Retry 1: all 1000 retry at t=1s → server gets 1000 requests → fails again
Retry 2: all 1000 retry at t=2s → same problem
→ Server never recovers because retries keep hammering it
✅ Exponential Backoff + Jitter
func retryWithBackoff(ctx context.Context, maxRetries int, fn func() error) error {
for attempt := 0; attempt < maxRetries; attempt++ {
err := fn()
if err == nil {
return nil
}
// Don't retry non-retriable errors
if isNonRetriable(err) {
return err // 400 Bad Request, 404 Not Found → don't retry
}
// Exponential backoff: 100ms, 200ms, 400ms, 800ms...
baseDelay := time.Duration(1<<uint(attempt)) * 100 * time.Millisecond
// Cap at 10 seconds
if baseDelay > 10*time.Second {
baseDelay = 10 * time.Second
}
// Full jitter: random delay between 0 and baseDelay
jitter := time.Duration(rand.Int63n(int64(baseDelay)))
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(jitter):
}
}
return fmt.Errorf("max retries exceeded")
}
func isNonRetriable(err error) bool {
// 4xx errors (except 429 Too Many Requests) are NOT retriable
var httpErr *HTTPError
if errors.As(err, &httpErr) {
return httpErr.StatusCode >= 400 && httpErr.StatusCode < 500 && httpErr.StatusCode != 429
}
return false
}Retry Budget
Problem: retry from every layer = retry amplification
Client → API GW (retry 3x) → Order Svc (retry 3x) → Payment Svc (retry 3x)
Worst case: 3 × 3 × 3 = 27 attempts for ONE user request!
Solution: Retry budget
- Track retry ratio per service
- If > 20% of requests are retries → STOP retrying
- Only the CLOSEST caller to the failing service should retry
- Outer layers should NOT retry (or retry only once)
Timeout Budgets
User expects response in 3 seconds.
API Gateway → User Svc → Order Svc → Payment Svc → DB
Total budget: 3000ms
Split:
API Gateway overhead: 100ms
User Service: 500ms (timeout for calling Order Svc)
Order Service: 1000ms (timeout for calling Payment Svc)
Payment Service: 800ms (timeout for DB + external API)
Buffer: 600ms
Total: 3000ms ✅
func OrderHandler(ctx context.Context) {
// Deadline propagation via context
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
// This timeout is RESPECTED by downstream calls
// If upstream already used 2s of a 3s budget,
// we only have 1s left — context carries this info
result, err := paymentClient.Charge(ctx, amount)
if ctx.Err() == context.DeadlineExceeded {
// Timed out — don't retry, return degraded response
return fallbackResponse()
}
}Bulkhead Pattern
Problem
Order Service calls:
- Payment Service (connection pool: shared)
- Inventory Service (connection pool: shared)
- Notification Service (connection pool: shared)
Payment Service goes slow → all connections stuck waiting for Payment
→ No connections left for Inventory or Notification
→ EVERYTHING fails even though Inventory and Notification are fine
Solution: Isolate Resources
┌─────────────────────────────────────────────┐
│ Order Service │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Payment Pool │ │ Inventory │ │
│ │ (50 conns) │ │ Pool (50) │ │
│ │ │ │ │ │
│ │ FULL/SLOW ⚠️ │ │ HEALTHY ✅ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Payment failing doesn't affect Inventory! │
└──────────────────────────────────────────────┘
// Semaphore-based bulkhead
type Bulkhead struct {
sem chan struct{}
}
func NewBulkhead(maxConcurrent int) *Bulkhead {
return &Bulkhead{sem: make(chan struct{}, maxConcurrent)}
}
func (b *Bulkhead) Execute(ctx context.Context, fn func() error) error {
select {
case b.sem <- struct{}{}:
defer func() { <-b.sem }()
return fn()
case <-ctx.Done():
return ctx.Err()
default:
return ErrBulkheadFull // fast fail, don't wait
}
}
// Per-dependency bulkheads
paymentBulkhead := NewBulkhead(50)
inventoryBulkhead := NewBulkhead(50)Graceful Degradation
"Poora feature band karo vs partial/degraded response do"
Examples:
1. Recommendation Service down:
❌ Show error page
✅ Show "Popular Items" (static/cached list)
2. Search Service slow:
❌ Wait 10 seconds
✅ After 2s timeout, show cached/trending results
3. Payment gateway down:
❌ "Payment failed, try again"
✅ Queue the payment, confirm later (async processing)
4. Profile picture service down:
❌ Show broken image
✅ Show default avatar
func GetProductPage(ctx context.Context, productID string) (*Page, error) {
// Core data — MUST succeed
product, err := productService.Get(ctx, productID)
if err != nil {
return nil, err // actual error, can't degrade
}
page := &Page{Product: product}
// Non-critical — degrade gracefully
reviews, err := reviewService.Get(ctx, productID)
if err != nil {
logger.Warn("reviews unavailable, using cached", zap.Error(err))
reviews = cachedReviews.Get(productID) // stale but okay
}
page.Reviews = reviews
// Non-critical — degrade gracefully
recommendations, err := recoService.Get(ctx, productID)
if err != nil {
logger.Warn("recommendations unavailable", zap.Error(err))
recommendations = defaultRecommendations()
}
page.Recommendations = recommendations
return page, nil
}Rate Limiting & Load Shedding
Rate Limiting — Inbound Traffic Control
// Token bucket rate limiter
import "golang.org/x/time/rate"
// Per-user rate limiter: 100 req/sec, burst of 200
limiter := rate.NewLimiter(rate.Limit(100), 200)
func RateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := extractUserID(r)
limiter := getUserLimiter(userID) // per-user limiter from sync.Map
if !limiter.Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, "Too Many Requests", 429)
return
}
next.ServeHTTP(w, r)
})
}Load Shedding — Jab Server Overloaded Ho
Rate limiting: "Har user ko max X req/sec allow karo"
Load shedding: "Server overloaded hai, kuch requests DROP karo"
Strategy: Priority-based shedding
1. Health checks → NEVER drop
2. Payment processing → HIGH priority
3. User-facing reads → MEDIUM priority
4. Analytics/logging → LOW priority (drop first)
Chaos Engineering
Principle
"Don't wait for production to surprise you. Break things intentionally."
1. Define steady state (normal metrics)
2. Hypothesize: "If X fails, Y should handle it"
3. Inject failure
4. Observe: Did the system handle it as expected?
5. Fix weaknesses found
Chaos Experiments
Network:
- Add 200ms latency to DB calls
- Drop 5% of packets between services
- DNS resolution failure
Infrastructure:
- Kill random pods (chaos monkey)
- Fill disk to 95%
- CPU stress on random nodes
Application:
- Force circuit breaker open
- Inject 500 errors from dependency
- Slow down one instance
Tools:
- Chaos Monkey (Netflix) — random instance termination
- Litmus Chaos (K8s native)
- Gremlin (commercial, comprehensive)
- toxiproxy (inject network issues)
Blast Radius Control
Start small → increase scope
Day 1: Kill 1 pod in staging
Day 7: Kill 1 pod in production (off-peak)
Day 30: Kill entire AZ in staging
Day 60: Kill entire AZ in production (off-peak, with rollback ready)
Rules:
✅ Always have abort button
✅ Run during business hours (not at 3am)
✅ Inform the team
✅ Have monitoring dashboards open
❌ Never in production without approval
❌ Never target stateful services first (DBs)
Production Resilience Checklist — 50M Users
Every service MUST have:
☐ Circuit breaker on all external calls
☐ Timeouts on every network call (no infinite waits)
☐ Retry with exponential backoff + jitter (only for retriable errors)
☐ Bulkhead isolation for independent dependencies
☐ Graceful degradation for non-critical features
☐ Rate limiting per user/API key
☐ Load shedding when overloaded
☐ Health check endpoints (liveness + readiness)
☐ Graceful shutdown (drain connections before exit)
Testing:
☐ Chaos experiments in staging monthly
☐ Gamedays (simulated incidents) quarterly
☐ Dependency failure tests in CI/CD pipeline
☐ Load tests at 2x expected peak before major releases