Retry Patterns: When and How to Try Again

Not all failures are permanent. Retry patterns help distinguish transient hiccups from real problems. Exponential Backoff 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import time import random def retry_with_backoff(func, max_retries=5, base_delay=1): for attempt in range(max_retries): try: return func() except Exception as e: if attempt == max_retries - 1: raise delay = base_delay * (2 ** attempt) jitter = random.uniform(0, delay * 0.1) time.sleep(delay + jitter) Each retry waits longer: 1s, 2s, 4s, 8s, 16s. Jitter prevents thundering herd. With tenacity 1 2 3 4 5 6 7 8 from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=60) ) def call_api(): return requests.get("https://api.example.com") Retry Only Transient Errors 1 2 3 4 5 6 7 8 from tenacity import retry, retry_if_exception_type @retry( retry=retry_if_exception_type((ConnectionError, TimeoutError)), stop=stop_after_attempt(3) ) def fetch_data(): return external_service.get() Don’t retry 400 Bad Request β€” that won’t fix itself. ...

February 28, 2026 Β· 2 min Β· 242 words Β· Rob Washington

Caching Strategies: What to Cache and When to Invalidate

Cache invalidation is one of the two hard problems in computer science. Here’s how to make it less painful. The Caching Patterns Cache-Aside (Lazy Loading) 1 2 3 4 5 6 7 8 9 10 11 12 13 def get_user(user_id: str) -> dict: # Check cache first cached = redis.get(f"user:{user_id}") if cached: return json.loads(cached) # Cache miss: fetch from database user = db.query("SELECT * FROM users WHERE id = %s", user_id) # Store in cache for next time redis.setex(f"user:{user_id}", 3600, json.dumps(user)) return user Pros: Only caches what’s actually used Cons: First request always slow (cache miss) ...

February 28, 2026 Β· 5 min Β· 955 words Β· Rob Washington

Graceful Shutdown: Stop Dropping Requests

Every deployment is a potential outage if your application doesn’t shut down gracefully. Here’s how to do it right. The Problem 1 2 3 4 5 . . . . . K P Y I U u o o n s b d u - e e r f r r i l s n s a i e p g s t r p h e e e t e s m e o x r e s v i e r e e t q r n d s u o d e r s f i s s r m t S o m s d I m e u G d g r T s i e i E e a t n R r t g M v e c i l o " c y n z e n e e r e c o n t - d i d p o o o n w i n n r t t e i s s m e e t " d e p l o y s The fix: handle SIGTERM, finish existing work, then exit. ...

February 28, 2026 Β· 5 min Β· 1065 words Β· Rob Washington

API Versioning Strategies That Don't Hurt

Your API will change. How you handle that change determines whether clients curse your name or barely notice. The Three Approaches 1. URL Path Versioning G G E E T T v 1 2 / / u u s s e e r r s s / / 1 1 2 2 3 3 Pros: ...

February 28, 2026 Β· 4 min Β· 791 words Β· Rob Washington

Background Job Patterns: Processing Work Outside the Request Cycle

Some work doesn’t belong in a web request. Sending emails, processing uploads, generating reports, syncing with external APIs β€” these tasks are too slow, too unreliable, or too resource-intensive to run while a user waits. Background jobs solve this by moving work out of the request cycle and into a separate processing system. The Basic Architecture β”Œ β”‚ β”” ─ ─ ─ W ─ ─ e ─ ─ b ─ ─ ─ β”‚ β”‚ β”” ─ A ─ ─ ─ p ─ ─ ─ p ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ─ ─ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” β–Ά ─ ─ β”Œ β”‚ β”” ─ R ─ ─ ─ ─ e ─ ─ Q ─ ─ s ─ ─ u ─ ─ u ─ ─ e ─ ─ l ─ ─ u ─ ─ t ─ ─ e ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ ┐ β”‚ β”˜ ─ β—€ ─ ─ ─ ─ ─ ─ β–Ά ─ β”Œ β”‚ β”” ─ ─ ─ ─ ─ W ─ ─ ─ o ─ ─ ─ r ─ β”‚ β”˜ ─ k ─ β”‚ ─ e ─ ─ r ─ ─ s ─ ─ ─ ─ ─ ┐ β”‚ β”˜ Producer: Web app enqueues jobs Queue: Stores jobs until workers are ready Workers: Process jobs independently Results: Optional storage for job outcomes Choosing a Queue Backend Redis (with Sidekiq, Bull, Celery) 1 2 3 4 5 6 7 8 9 # Celery with Redis from celery import Celery app = Celery('tasks', broker='redis://localhost:6379/0') @app.task def send_email(user_id, template): user = get_user(user_id) email_service.send(user.email, template) Pros: Fast, simple, good ecosystem Cons: Not durable by default (can lose jobs on crash) ...

February 24, 2026 Β· 7 min Β· 1300 words Β· Rob Washington

API Pagination Patterns: Getting Large Result Sets Right

Every API eventually needs to return more data than fits in a single response. How you handle that pagination affects performance, reliability, and developer experience. Let’s look at the common patterns, their tradeoffs, and when to use each. The Three Main Approaches 1. Offset Pagination The classic approach: skip N records, return M records. G G G E E E T T T / / / a a a p p p i i i / / / i i i t t t e e e m m m s s s ? ? ? o o o f f f f f f s s s e e e t t t = = = 0 2 4 & 0 0 l & & i l l m i i i m m t i i = t t 2 = = 0 2 2 0 0 # # # P P P a a a g g g e e e 1 2 3 Implementation: ...

February 24, 2026 Β· 9 min Β· 1726 words Β· Rob Washington

Error Handling Philosophy: Fail Gracefully, Recover Quickly

Errors are inevitable. Networks fail. Disks fill up. Services crash. Users input garbage. The question isn’t whether your system will encounter errors β€” it’s how it will behave when it does. Good error handling is the difference between β€œthe system recovered automatically” and β€œwe lost customer data.” Fail Fast, Fail Loud Detect problems early and report them clearly: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # ❌ Bad: Silent failure def process_order(order): try: result = payment_service.charge(order) except Exception: pass # Swallow error, pretend it worked return {"status": "ok"} # βœ… Good: Fail fast with clear error def process_order(order): if not order.items: raise ValidationError("Order must have at least one item") if order.total <= 0: raise ValidationError("Order total must be positive") result = payment_service.charge(order) # Let it fail if it fails return {"status": "ok", "charge_id": result.id} Silent failures are debugging nightmares. If something goes wrong, make it obvious. ...

February 24, 2026 Β· 6 min Β· 1159 words Β· Rob Washington

JWT Authentication Done Right: Tokens Without the Footguns

JSON Web Tokens are everywhere in modern authentication. They’re stateless, portable, and self-contained. They’re also easy to implement insecurely. These practices help you use JWTs without shooting yourself in the foot. JWT Structure A JWT has three parts, base64-encoded and dot-separated: h e e y a J d h e b r G . c p i a O y i l J o I a U d z . I s 1 i N g i n J a 9 t . u e r y e J z d W I i O i I x M j M i f Q . s I G 5 n 8 l 7 . . . Header: ...

February 24, 2026 Β· 6 min Β· 1177 words Β· Rob Washington

Async Python Patterns: Concurrency Without the Confusion

Async Python lets you handle thousands of concurrent I/O operations with a single thread. No threads, no processes, no GIL headaches. But it requires thinking differently about how code executes. These patterns help you write async code that’s both correct and efficient. The Basics 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import asyncio async def fetch_data(url: str) -> dict: # This is a coroutine - it can be paused and resumed async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.json() # Running coroutines async def main(): data = await fetch_data("https://api.example.com/data") print(data) asyncio.run(main()) await pauses the coroutine until the result is ready, letting other coroutines run. ...

February 23, 2026 Β· 6 min Β· 1092 words Β· Rob Washington

Feature Flags: Deploy Without Fear

Deployment and release are different things. Deployment puts code on servers. Release makes features available to users. Feature flags separate these concerns. With feature flags, you can deploy daily but release weekly. Ship risky changes but only enable them for 1% of users. Roll back a broken feature in seconds without touching infrastructure. Basic Implementation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class FeatureFlags: def __init__(self): self.flags = {} def is_enabled(self, flag_name: str, user_id: str = None) -> bool: flag = self.flags.get(flag_name) if not flag: return False # Global kill switch if not flag.get("enabled", False): return False # Check user allowlist if user_id and user_id in flag.get("allowed_users", []): return True # Check percentage rollout if user_id and flag.get("percentage", 0) > 0: # Consistent hashing: same user always gets same result hash_val = hash(f"{flag_name}:{user_id}") % 100 return hash_val < flag["percentage"] return flag.get("enabled", False) flags = FeatureFlags() flags.flags = { "new_checkout": { "enabled": True, "percentage": 10, # 10% of users "allowed_users": ["user_123"] # Plus specific users } } # Usage if flags.is_enabled("new_checkout", user_id=current_user.id): return new_checkout_flow() else: return old_checkout_flow() Configuration-Based Flags Store flags in config, not code: ...

February 23, 2026 Β· 7 min Β· 1351 words Β· Rob Washington