Skip to content

System Resilience, Rate Limiting, Concurrency Control & Backpressure: Best Practices for Distributed Systems

Objective: Master production-grade resilience patterns for distributed systems. When you need to prevent cascading failures, control concurrency, implement rate limiting, handle backpressure, and maintain SLOs under load—this guide provides complete patterns and implementations.

Introduction

Distributed systems face constant pressure: traffic spikes, resource exhaustion, network failures, and cascading errors. Without proper resilience patterns, systems fail catastrophically. This guide provides a complete framework for building resilient, high-performance distributed systems.

What This Guide Covers: - Concurrency control (threads, async, worker pools) - Rate limiting (token bucket, leaky bucket, sliding windows) - Backpressure mechanisms - Load shedding and graceful degradation - Circuit breakers and bulkheads - SLO-driven design and error budgets - Autoscaling strategies - GIS and ML-specific resilience patterns

Prerequisites: - Understanding of distributed systems, microservices, and concurrency - Familiarity with Python async, Go concurrency, Rust async - Experience with Kubernetes, databases, and message queues

Purpose & Importance

Why Resilience Patterns Are Essential

Postgres-Heavy Systems: - Connection pool exhaustion causes cascading failures - Slow queries block all requests - Replication lag causes read inconsistencies - Without rate limiting, databases are overwhelmed

GIS Computation Pipelines: - Spatial queries are CPU-intensive and slow - Tile generation can saturate compute resources - Isochrone calculations are expensive - Without backpressure, GIS services become unresponsive

ML/ONNX Inference Flows: - Model loading is expensive (cold starts) - GPU saturation causes queue buildup - Feature extraction can be slow - Without load shedding, inference latency degrades

Real-Time Dashboards: - Grafana queries can overwhelm databases - NiceGUI WebSocket connections consume resources - Real-time updates create thundering herd - Without rate limiting, dashboards become unusable

ETL & Streaming Workloads: - Large datasets overwhelm memory - Streaming backpressure causes data loss - Pipeline failures cascade downstream - Without circuit breakers, entire pipelines fail

Microservice Architectures: - Service failures cascade across dependencies - Network partitions cause timeouts - Retry storms overwhelm services - Without isolation, one service failure takes down all

Air-Gapped Clusters: - Limited compute resources - No external fallbacks - Resource contention is severe - Without load shedding, systems become unusable

Failure Cascade Without Resilience

graph TD
    Start["Traffic Spike"] --> API["API Service<br/>(No Rate Limit)"]
    API -->|"Unlimited Requests"| PG["Postgres<br/>(Connection Exhaustion)"]
    API -->|"Unlimited Requests"| Redis["Redis<br/>(Memory Exhaustion)"]
    API -->|"Unlimited Requests"| ML["ML Service<br/>(GPU Saturation)"]

    PG -->|"Slow Queries"| API2["API Service<br/>(Request Timeout)"]
    Redis -->|"Evictions"| API3["API Service<br/>(Cache Misses)"]
    ML -->|"Queue Buildup"| API4["API Service<br/>(Latency Spike)"]

    API2 -->|"Cascading Failures"| Down["System Down"]
    API3 -->|"Cascading Failures"| Down
    API4 -->|"Cascading Failures"| Down

    style Start fill:#ffcccc
    style Down fill:#ff0000

Failure Cascade With Resilience

graph TD
    Start["Traffic Spike"] --> RL["Rate Limiter<br/>(Token Bucket)"]
    RL -->|"Throttled Requests"| API["API Service<br/>(Controlled Load)"]

    API -->|"Pooled Connections"| PG["Postgres<br/>(Connection Pool)"]
    API -->|"Cached Responses"| Redis["Redis<br/>(TTL + Eviction)"]
    API -->|"Queued Requests"| ML["ML Service<br/>(Worker Pool)"]

    PG -->|"Slow Query"| CB["Circuit Breaker<br/>(Opens)"]
    CB -->|"Fallback"| Cache["Cache Response"]

    ML -->|"Queue Full"| BP["Backpressure<br/>(Reject New)"]
    BP -->|"Load Shed"| LS["Load Shedder<br/>(Drop Low Priority)"]

    Cache -->|"Graceful Degradation"| Stable["System Stable"]
    LS -->|"Graceful Degradation"| Stable

    style Start fill:#ffcccc
    style Stable fill:#ccffcc

Core Concepts

Concurrency Control

Thread-Level Concurrency

Python Threading:

# concurrency/thread_pool.py
from concurrent.futures import ThreadPoolExecutor
import threading

class ThreadPoolController:
    def __init__(self, max_workers: int = 10):
        self.max_workers = max_workers
        self.executor = ThreadPoolExecutor(max_workers=max_workers)
        self.semaphore = threading.Semaphore(max_workers)

    def submit(self, func, *args, **kwargs):
        """Submit task with concurrency control"""
        with self.semaphore:
            return self.executor.submit(func, *args, **kwargs)

    def shutdown(self, wait=True):
        """Shutdown thread pool"""
        self.executor.shutdown(wait=wait)

# Usage
pool = ThreadPoolController(max_workers=10)
future = pool.submit(expensive_operation, arg1, arg2)
result = future.result()

Go Goroutine Pool:

// concurrency/worker_pool.go
package concurrency

import (
    "context"
    "sync"
)

type WorkerPool struct {
    workers    int
    jobQueue   chan func()
    wg         sync.WaitGroup
}

func NewWorkerPool(workers int, queueSize int) *WorkerPool {
    return &WorkerPool{
        workers:  workers,
        jobQueue: make(chan func(), queueSize),
    }
}

func (wp *WorkerPool) Start(ctx context.Context) {
    for i := 0; i < wp.workers; i++ {
        wp.wg.Add(1)
        go func() {
            defer wp.wg.Done()
            for {
                select {
                case job := <-wp.jobQueue:
                    job()
                case <-ctx.Done():
                    return
                }
            }
        }()
    }
}

func (wp *WorkerPool) Submit(job func()) error {
    select {
    case wp.jobQueue <- job:
        return nil
    default:
        return ErrQueueFull
    }
}

func (wp *WorkerPool) Stop() {
    close(wp.jobQueue)
    wp.wg.Wait()
}

Rust Tokio Concurrency:

// concurrency/tokio_pool.rs
use tokio::sync::Semaphore;
use std::sync::Arc;

pub struct ConcurrencyLimiter {
    semaphore: Arc<Semaphore>,
}

impl ConcurrencyLimiter {
    pub fn new(max_concurrent: usize) -> Self {
        Self {
            semaphore: Arc::new(Semaphore::new(max_concurrent)),
        }
    }

    pub async fn execute<F, T>(&self, f: F) -> T
    where
        F: Future<Output = T>,
    {
        let _permit = self.semaphore.acquire().await.unwrap();
        f.await
    }
}

// Usage
let limiter = ConcurrencyLimiter::new(10);
let result = limiter.execute(async {
    expensive_operation().await
}).await;

Async Scheduling

Python asyncio:

# concurrency/async_limiter.py
import asyncio
from asyncio import Semaphore

class AsyncLimiter:
    def __init__(self, max_concurrent: int = 10):
        self.semaphore = Semaphore(max_concurrent)

    async def execute(self, coro):
        """Execute coroutine with concurrency limit"""
        async with self.semaphore:
            return await coro

# Usage
limiter = AsyncLimiter(max_concurrent=10)
results = await asyncio.gather(*[
    limiter.execute(process_item(item))
    for item in items
])

Worker Pools

Python Worker Pool:

# concurrency/worker_pool.py
import asyncio
from asyncio import Queue

class AsyncWorkerPool:
    def __init__(self, num_workers: int, queue_size: int = 100):
        self.num_workers = num_workers
        self.queue = Queue(maxsize=queue_size)
        self.workers = []

    async def start(self):
        """Start worker pool"""
        self.workers = [
            asyncio.create_task(self._worker())
            for _ in range(self.num_workers)
        ]

    async def _worker(self):
        """Worker coroutine"""
        while True:
            item = await self.queue.get()
            if item is None:  # Shutdown signal
                break
            await self.process_item(item)
            self.queue.task_done()

    async def submit(self, item):
        """Submit item to queue"""
        await self.queue.put(item)

    async def shutdown(self):
        """Shutdown worker pool"""
        for _ in range(self.num_workers):
            await self.queue.put(None)
        await asyncio.gather(*self.workers)

CPU-Bound vs IO-Bound Decisions

Decision Matrix:

Workload Type Concurrency Model Example
CPU-bound Thread pool (limited by CPU cores) Image processing, ML inference
IO-bound Async/await (many concurrent) Database queries, API calls
Mixed Hybrid (thread pool + async) ETL with CPU transforms + DB I/O

Implementation:

# concurrency/cpu_vs_io.py
import asyncio
from concurrent.futures import ThreadPoolExecutor
import aiohttp

class HybridExecutor:
    def __init__(self, cpu_workers: int = 4, io_concurrent: int = 100):
        self.cpu_pool = ThreadPoolExecutor(max_workers=cpu_workers)
        self.io_semaphore = asyncio.Semaphore(io_concurrent)

    async def cpu_bound_task(self, func, *args):
        """Execute CPU-bound task in thread pool"""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(self.cpu_pool, func, *args)

    async def io_bound_task(self, coro):
        """Execute IO-bound task with concurrency limit"""
        async with self.io_semaphore:
            return await coro

# Usage
executor = HybridExecutor()

# CPU-bound: ML inference
result = await executor.cpu_bound_task(model.predict, features)

# IO-bound: API call
response = await executor.io_bound_task(session.get(url))

Controlling Parallel Geospatial Computations

Pattern: Limit concurrent spatial operations.

# concurrency/geospatial_limiter.py
import asyncio
from shapely.geometry import LineString

class GeospatialLimiter:
    def __init__(self, max_concurrent: int = 4):
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def compute_intersection(self, geom1, geom2):
        """Compute spatial intersection with concurrency limit"""
        async with self.semaphore:
            # CPU-bound operation in thread pool
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(
                None,
                lambda: geom1.intersects(geom2)
            )

# Usage
limiter = GeospatialLimiter(max_concurrent=4)
results = await asyncio.gather(*[
    limiter.compute_intersection(road, zone)
    for road, zone in road_zone_pairs
])

Concurrency in Go, Python Async, Rust Async

Comparison Table:

Language Concurrency Model Best For
Go Goroutines + channels High-throughput, simple concurrency
Python async asyncio + coroutines IO-bound, event-driven
Rust async Tokio + futures High-performance, memory-safe

Go Example:

// concurrency/go_concurrency.go
func processConcurrently(items []Item) []Result {
    results := make([]Result, len(items))
    sem := make(chan struct{}, 10) // Max 10 concurrent
    var wg sync.WaitGroup

    for i, item := range items {
        wg.Add(1)
        go func(idx int, it Item) {
            defer wg.Done()
            sem <- struct{}{}        // Acquire
            defer func() { <-sem }() // Release

            results[idx] = processItem(it)
        }(i, item)
    }

    wg.Wait()
    return results
}

Python Async Example:

# concurrency/python_async.py
async def process_concurrently(items: list) -> list:
    semaphore = asyncio.Semaphore(10)  # Max 10 concurrent

    async def process_item(item):
        async with semaphore:
            return await process_item_async(item)

    return await asyncio.gather(*[
        process_item(item) for item in items
    ])

Rust Async Example:

// concurrency/rust_async.rs
use tokio::sync::Semaphore;

async fn process_concurrently(items: Vec<Item>) -> Vec<Result> {
    let semaphore = Arc::new(Semaphore::new(10)); // Max 10 concurrent

    let tasks: Vec<_> = items.into_iter().map(|item| {
        let sem = semaphore.clone();
        tokio::spawn(async move {
            let _permit = sem.acquire().await.unwrap();
            process_item(item).await
        })
    }).collect();

    futures::future::join_all(tasks).await
        .into_iter()
        .map(|r| r.unwrap())
        .collect()
}

Controlling ONNX Inference Concurrency

Pattern: Limit concurrent model inference.

# concurrency/onnx_limiter.py
import onnxruntime as ort
import asyncio

class ONNXInferenceLimiter:
    def __init__(self, model_path: str, max_concurrent: int = 2):
        self.session = ort.InferenceSession(model_path)
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def infer(self, input_data: np.ndarray):
        """Run inference with concurrency limit"""
        async with self.semaphore:
            # Run inference (CPU-bound, but async for concurrency control)
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(
                None,
                lambda: self.session.run(None, {"input": input_data})
            )

Rate Limiting Patterns

Token Bucket

Algorithm: Tokens added at fixed rate, requests consume tokens.

Python Implementation:

# rate_limiting/token_bucket.py
import time
import asyncio
from collections import deque

class TokenBucket:
    def __init__(self, capacity: int, refill_rate: float):
        """
        capacity: Maximum tokens
        refill_rate: Tokens per second
        """
        self.capacity = capacity
        self.refill_rate = refill_rate
        self.tokens = capacity
        self.last_refill = time.time()
        self.lock = asyncio.Lock()

    async def acquire(self, tokens: int = 1) -> bool:
        """Try to acquire tokens"""
        async with self.lock:
            self._refill()

            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False

    def _refill(self):
        """Refill tokens based on elapsed time"""
        now = time.time()
        elapsed = now - self.last_refill
        self.tokens = min(
            self.capacity,
            self.tokens + elapsed * self.refill_rate
        )
        self.last_refill = now

# Usage
bucket = TokenBucket(capacity=100, refill_rate=10.0)  # 10 tokens/sec

async def make_request():
    if await bucket.acquire():
        # Make request
        pass
    else:
        # Rate limited
        raise RateLimitError()

Redis-Based Token Bucket:

# rate_limiting/redis_token_bucket.py
import redis
import time

class RedisTokenBucket:
    def __init__(self, redis_client: redis.Redis, key: str, capacity: int, refill_rate: float):
        self.redis = redis_client
        self.key = key
        self.capacity = capacity
        self.refill_rate = refill_rate

    def acquire(self, tokens: int = 1) -> bool:
        """Acquire tokens using Redis Lua script"""
        lua_script = """
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local refill_rate = tonumber(ARGV[2])
        local tokens_requested = tonumber(ARGV[3])
        local now = tonumber(ARGV[4])

        local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
        local current_tokens = tonumber(bucket[1]) or capacity
        local last_refill = tonumber(bucket[2]) or now

        -- Refill tokens
        local elapsed = now - last_refill
        current_tokens = math.min(capacity, current_tokens + elapsed * refill_rate)

        -- Check if enough tokens
        if current_tokens >= tokens_requested then
            current_tokens = current_tokens - tokens_requested
            redis.call('HMSET', key, 'tokens', current_tokens, 'last_refill', now)
            redis.call('EXPIRE', key, 3600)
            return 1
        else
            redis.call('HMSET', key, 'tokens', current_tokens, 'last_refill', now)
            redis.call('EXPIRE', key, 3600)
            return 0
        end
        """

        result = self.redis.eval(
            lua_script,
            1,
            self.key,
            self.capacity,
            self.refill_rate,
            tokens,
            int(time.time())
        )

        return bool(result)

Leaky Bucket

Algorithm: Requests added to bucket, processed at fixed rate.

Python Implementation:

# rate_limiting/leaky_bucket.py
import asyncio
import time
from collections import deque

class LeakyBucket:
    def __init__(self, capacity: int, leak_rate: float):
        """
        capacity: Maximum queue size
        leak_rate: Requests per second processed
        """
        self.capacity = capacity
        self.leak_rate = leak_rate
        self.queue = deque()
        self.last_leak = time.time()
        self.lock = asyncio.Lock()

    async def add(self, item) -> bool:
        """Add item to bucket"""
        async with self.lock:
            if len(self.queue) >= self.capacity:
                return False  # Bucket full

            self.queue.append((item, time.time()))
            return True

    async def process(self):
        """Process items from bucket at leak rate"""
        while True:
            async with self.lock:
                self._leak()

            await asyncio.sleep(1.0 / self.leak_rate)

    def _leak(self):
        """Remove items based on leak rate"""
        now = time.time()
        elapsed = now - self.last_leak
        items_to_process = int(elapsed * self.leak_rate)

        for _ in range(min(items_to_process, len(self.queue))):
            if self.queue:
                item, _ = self.queue.popleft()
                # Process item
                asyncio.create_task(process_item(item))

        self.last_leak = now

Fixed Window

Algorithm: Count requests in fixed time window.

Python Implementation:

# rate_limiting/fixed_window.py
import time
from collections import defaultdict

class FixedWindowLimiter:
    def __init__(self, max_requests: int, window_seconds: int):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.windows = defaultdict(int)
        self.window_start = defaultdict(int)

    def is_allowed(self, key: str) -> bool:
        """Check if request is allowed"""
        now = int(time.time())
        window = now // self.window_seconds

        # Reset if new window
        if self.window_start[key] != window:
            self.windows[key] = 0
            self.window_start[key] = window

        # Check limit
        if self.windows[key] < self.max_requests:
            self.windows[key] += 1
            return True

        return False

Redis Fixed Window:

# rate_limiting/redis_fixed_window.py
import redis
import time

class RedisFixedWindowLimiter:
    def __init__(self, redis_client: redis.Redis, max_requests: int, window_seconds: int):
        self.redis = redis_client
        self.max_requests = max_requests
        self.window_seconds = window_seconds

    def is_allowed(self, key: str) -> bool:
        """Check if request is allowed using Redis"""
        window = int(time.time()) // self.window_seconds
        redis_key = f"rate_limit:{key}:{window}"

        current = self.redis.incr(redis_key)
        self.redis.expire(redis_key, self.window_seconds)

        return current <= self.max_requests

Sliding Log / Rolling Window

Algorithm: Track individual request timestamps, count in sliding window.

Python Implementation:

# rate_limiting/sliding_log.py
import time
from collections import deque

class SlidingLogLimiter:
    def __init__(self, max_requests: int, window_seconds: int):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.logs = defaultdict(deque)

    def is_allowed(self, key: str) -> bool:
        """Check if request is allowed"""
        now = time.time()
        window_start = now - self.window_seconds

        # Remove old entries
        log = self.logs[key]
        while log and log[0] < window_start:
            log.popleft()

        # Check limit
        if len(log) < self.max_requests:
            log.append(now)
            return True

        return False

Redis Sliding Log:

# rate_limiting/redis_sliding_log.py
import redis
import time

class RedisSlidingLogLimiter:
    def __init__(self, redis_client: redis.Redis, max_requests: int, window_seconds: int):
        self.redis = redis_client
        self.max_requests = max_requests
        self.window_seconds = window_seconds

    def is_allowed(self, key: str) -> bool:
        """Check if request is allowed using Redis sorted set"""
        now = time.time()
        window_start = now - self.window_seconds
        redis_key = f"rate_limit:sliding:{key}"

        # Remove old entries
        self.redis.zremrangebyscore(redis_key, 0, window_start)

        # Count current entries
        current = self.redis.zcard(redis_key)

        if current < self.max_requests:
            # Add current request
            self.redis.zadd(redis_key, {str(now): now})
            self.redis.expire(redis_key, self.window_seconds)
            return True

        return False

NGINX-Based Rate Limits

NGINX Configuration:

# nginx/rate_limiting.conf
http {
    # Define rate limit zones
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_req_zone $http_authorization zone=user_limit:10m rate=100r/s;

    # Per-IP rate limiting
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    server {
        listen 80;
        server_name api.example.com;

        # Apply rate limits
        limit_req zone=api_limit burst=20 nodelay;
        limit_conn conn_limit 10;

        location /api/ {
            proxy_pass http://backend;

            # Per-user rate limiting (if auth header present)
            limit_req zone=user_limit burst=50 nodelay;
        }

        # Stricter limits for expensive endpoints
        location /api/ml/infer {
            limit_req zone=api_limit burst=5 nodelay;
            proxy_pass http://ml-backend;
        }
    }
}

Per-User, Per-IP, Per-Org, Per-Service Policies

Multi-Level Rate Limiting:

# rate_limiting/multi_level.py
class MultiLevelRateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
        self.limiters = {
            "ip": RedisFixedWindowLimiter(redis_client, 100, 60),      # 100/min per IP
            "user": RedisFixedWindowLimiter(redis_client, 1000, 60),   # 1000/min per user
            "org": RedisFixedWindowLimiter(redis_client, 10000, 60),   # 10000/min per org
            "service": RedisFixedWindowLimiter(redis_client, 100000, 60)  # 100000/min per service
        }

    def is_allowed(self, ip: str, user_id: str, org_id: str, service: str) -> bool:
        """Check all rate limits"""
        checks = [
            self.limiters["ip"].is_allowed(f"ip:{ip}"),
            self.limiters["user"].is_allowed(f"user:{user_id}"),
            self.limiters["org"].is_allowed(f"org:{org_id}"),
            self.limiters["service"].is_allowed(f"service:{service}")
        ]

        return all(checks)

Rate Limiting Downstream Resources

Protecting Postgres:

# rate_limiting/protect_postgres.py
class PostgresRateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
        self.limiter = RedisTokenBucket(
            redis_client,
            "postgres:rate_limit",
            capacity=1000,      # Max 1000 queries
            refill_rate=100.0   # 100 queries/sec
        )

    async def execute_query(self, query: str, params: tuple):
        """Execute query with rate limiting"""
        if not await self.limiter.acquire():
            raise RateLimitError("Postgres rate limit exceeded")

        return await db.execute(query, params)

Backpressure Mechanisms

Queue Size Controls

Pattern: Reject requests when queue is full.

# backpressure/queue_control.py
import asyncio
from asyncio import Queue, QueueFull

class BackpressureQueue:
    def __init__(self, maxsize: int = 100):
        self.queue = Queue(maxsize=maxsize)
        self.maxsize = maxsize

    async def put(self, item, timeout: float = 0.1) -> bool:
        """Put item with timeout"""
        try:
            await asyncio.wait_for(self.queue.put(item), timeout=timeout)
            return True
        except asyncio.TimeoutError:
            return False  # Queue full, backpressure

    async def get(self):
        """Get item from queue"""
        return await self.queue.get()

Async Task Backpressure

Pattern: Limit concurrent async tasks.

# backpressure/async_backpressure.py
class AsyncBackpressure:
    def __init__(self, max_concurrent: int = 10, max_queue: int = 100):
        self.semaphore = asyncio.Semaphore(max_concurrent)
        self.queue = asyncio.Queue(maxsize=max_queue)

    async def submit(self, coro) -> bool:
        """Submit task with backpressure"""
        try:
            await asyncio.wait_for(self.queue.put(coro), timeout=0.1)
            asyncio.create_task(self._process())
            return True
        except asyncio.TimeoutError:
            return False  # Backpressure: queue full

    async def _process(self):
        """Process tasks from queue"""
        async with self.semaphore:
            coro = await self.queue.get()
            await coro

Redis Streams Backpressure

Pattern: Monitor stream length, reject when too high.

# backpressure/redis_streams.py
class RedisStreamBackpressure:
    def __init__(self, redis_client: redis.Redis, stream_name: str, max_length: int = 10000):
        self.redis = redis_client
        self.stream_name = stream_name
        self.max_length = max_length

    async def add_message(self, fields: dict) -> bool:
        """Add message with backpressure check"""
        # Check stream length
        length = self.redis.xlen(self.stream_name)

        if length >= self.max_length:
            return False  # Backpressure: stream too long

        # Add message
        self.redis.xadd(self.stream_name, fields, maxlen=self.max_length)
        return True

Prefect/Dask Backpressure

Prefect Task Concurrency Limits:

# backpressure/prefect_backpressure.py
from prefect import task, flow
from prefect.concurrency import concurrency

@task
@concurrency(limit=10, occupy=1)  # Max 10 concurrent
def expensive_task(data: dict):
    """Task with concurrency limit"""
    return process_data(data)

@flow
def etl_flow():
    """ETL flow with backpressure"""
    items = load_items()

    # Process with concurrency limit (backpressure)
    results = []
    for item in items:
        result = expensive_task(item)  # Will queue if limit reached
        results.append(result)

    return results

Dask Backpressure:

# backpressure/dask_backpressure.py
from dask.distributed import Client
from dask import delayed

def process_with_backpressure(client: Client, items: list, max_concurrent: int = 10):
    """Process items with Dask backpressure"""
    # Limit concurrent tasks
    semaphore = asyncio.Semaphore(max_concurrent)

    @delayed
    def process_item(item):
        return expensive_operation(item)

    # Process with limit
    tasks = [process_item(item) for item in items]
    results = client.compute(tasks, resources={"worker": 1})

    return results

Postgres Connection Pool Saturation

Pattern: Reject connections when pool is full.

# backpressure/postgres_pool.py
import asyncpg
from asyncpg import Pool

class PostgresPoolWithBackpressure:
    def __init__(self, dsn: str, min_size: int = 5, max_size: int = 20):
        self.pool: Pool = None
        self.dsn = dsn
        self.min_size = min_size
        self.max_size = max_size

    async def connect(self):
        """Create connection pool"""
        self.pool = await asyncpg.create_pool(
            self.dsn,
            min_size=self.min_size,
            max_size=self.max_size
        )

    async def execute(self, query: str, *args, timeout: float = 5.0):
        """Execute query with timeout and backpressure"""
        try:
            async with self.pool.acquire(timeout=timeout) as conn:
                return await conn.execute(query, *args)
        except asyncio.TimeoutError:
            raise BackpressureError("Connection pool exhausted")

Postgres max_connections:

-- postgresql.conf
max_connections = 100
superuser_reserved_connections = 3

-- Per-database connection limits
ALTER DATABASE mydb CONNECTION LIMIT 50;

GIS Tile Generation Throttling

Pattern: Limit concurrent tile generation.

# backpressure/gis_tile_throttle.py
class TileGenerationThrottle:
    def __init__(self, max_concurrent: int = 4):
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def generate_tile(self, z: int, x: int, y: int):
        """Generate tile with throttling"""
        async with self.semaphore:
            return await generate_tile_async(z, x, y)

Model Inference Backpressure

Pattern: Queue inference requests, reject when queue full.

# backpressure/ml_inference.py
class MLInferenceBackpressure:
    def __init__(self, model, max_queue: int = 100, max_concurrent: int = 2):
        self.model = model
        self.queue = asyncio.Queue(maxsize=max_queue)
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def infer(self, input_data: np.ndarray, timeout: float = 5.0) -> np.ndarray:
        """Run inference with backpressure"""
        try:
            # Try to add to queue
            future = asyncio.Future()
            await asyncio.wait_for(self.queue.put((input_data, future)), timeout=0.1)

            # Process inference
            asyncio.create_task(self._process_inference())

            # Wait for result
            return await asyncio.wait_for(future, timeout=timeout)
        except asyncio.TimeoutError:
            raise BackpressureError("Inference queue full")

    async def _process_inference(self):
        """Process inference requests"""
        async with self.semaphore:
            input_data, future = await self.queue.get()
            result = self.model.predict(input_data)
            future.set_result(result)

Load Shedding

Soft vs Hard Shedding

Soft Shedding: Delay or degrade service.

Hard Shedding: Reject requests immediately.

# load_shedding/shedding_strategy.py
class LoadShedder:
    def __init__(self, soft_threshold: float = 0.8, hard_threshold: float = 0.95):
        self.soft_threshold = soft_threshold
        self.hard_threshold = hard_threshold

    def should_shed(self, load: float, priority: str = "normal") -> str:
        """Determine shedding strategy"""
        if load >= self.hard_threshold:
            return "hard"  # Reject immediately

        if load >= self.soft_threshold:
            if priority == "low":
                return "soft"  # Delay or degrade
            return "none"

        return "none"

Prioritized Workloads

Pattern: Shed low-priority requests first.

# load_shedding/priority_shedding.py
from enum import Enum

class Priority(Enum):
    CRITICAL = 1
    HIGH = 2
    NORMAL = 3
    LOW = 4

class PriorityLoadShedder:
    def __init__(self, max_load: float = 0.9):
        self.max_load = max_load

    def should_process(self, priority: Priority, current_load: float) -> bool:
        """Determine if request should be processed"""
        if current_load < self.max_load:
            return True

        # Shed based on priority
        if priority == Priority.CRITICAL:
            return True
        elif priority == Priority.HIGH and current_load < 0.95:
            return True
        elif priority == Priority.NORMAL and current_load < 0.85:
            return True
        else:
            return False  # Shed low priority

Shedding Low-Priority Traffic

Implementation:

# load_shedding/traffic_shedding.py
class TrafficShedder:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
        self.load_monitor = LoadMonitor()

    async def handle_request(self, request: Request) -> Response:
        """Handle request with load shedding"""
        current_load = await self.load_monitor.get_load()
        priority = request.headers.get("X-Priority", "normal")

        if not self.should_process(priority, current_load):
            return Response(
                status_code=503,
                body={"error": "Service overloaded, please retry later"}
            )

        return await process_request(request)

Shedding ML Inference When Models Are Cold

Pattern: Reject requests when model is loading.

# load_shedding/ml_cold_start.py
class MLColdStartShedder:
    def __init__(self, model_loader):
        self.model_loader = model_loader
        self.model_loaded = asyncio.Event()
        self.loading = False

    async def load_model(self):
        """Load model asynchronously"""
        self.loading = True
        try:
            await self.model_loader.load()
            self.model_loaded.set()
        finally:
            self.loading = False

    async def infer(self, input_data: np.ndarray) -> np.ndarray:
        """Run inference, shed if model not loaded"""
        if not self.model_loaded.is_set():
            if self.loading:
                raise ServiceUnavailableError("Model loading, please retry")
            else:
                # Start loading
                asyncio.create_task(self.load_model())
                raise ServiceUnavailableError("Model not loaded")

        return await self.model.infer(input_data)

Dropping Expensive Geospatial Queries

Pattern: Reject expensive spatial queries under load.

# load_shedding/geospatial_shedding.py
class GeospatialLoadShedder:
    def __init__(self, max_concurrent_queries: int = 10):
        self.semaphore = asyncio.Semaphore(max_concurrent_queries)
        self.queue_size = 0

    async def execute_query(self, query: str, timeout: float = 30.0) -> dict:
        """Execute geospatial query with load shedding"""
        # Check if we can accept query
        if self.queue_size > 50:  # Threshold
            raise LoadShedError("Too many geospatial queries in queue")

        try:
            async with asyncio.timeout(timeout):
                async with self.semaphore:
                    self.queue_size += 1
                    try:
                        return await execute_spatial_query(query)
                    finally:
                        self.queue_size -= 1
        except asyncio.TimeoutError:
            raise LoadShedError("Geospatial query timeout")

Returning Partial or Approximate Results

Pattern: Return cached or approximate results under load.

# load_shedding/approximate_results.py
class ApproximateResultShedder:
    def __init__(self, cache_client: redis.Redis):
        self.cache = cache_client

    async def get_result(self, query: str, exact: bool = True) -> dict:
        """Get result, return approximate if under load"""
        current_load = await get_system_load()

        if current_load > 0.9 and not exact:
            # Return cached approximate result
            cached = self.cache.get(f"approx:{hash(query)}")
            if cached:
                return json.loads(cached)

        # Compute exact result
        result = await compute_exact_result(query)

        # Cache for future approximate results
        self.cache.setex(
            f"approx:{hash(query)}",
            3600,
            json.dumps(result)
        )

        return result

Circuit Breakers

Open/Half-Open/Closed States

State Machine:

stateDiagram-v2
    [*] --> Closed: Initial
    Closed --> Open: Failure threshold exceeded
    Open --> HalfOpen: Timeout elapsed
    HalfOpen --> Closed: Success
    HalfOpen --> Open: Failure

Python Implementation:

# circuit_breaker/circuit_breaker.py
import asyncio
import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    def __init__(
        self,
        failure_threshold: int = 5,
        success_threshold: int = 2,
        timeout: float = 60.0
    ):
        self.failure_threshold = failure_threshold
        self.success_threshold = success_threshold
        self.timeout = timeout

        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.success_count = 0
        self.last_failure_time = None
        self.lock = asyncio.Lock()

    async def call(self, func, *args, **kwargs):
        """Call function with circuit breaker"""
        async with self.lock:
            if self.state == CircuitState.OPEN:
                if time.time() - self.last_failure_time > self.timeout:
                    self.state = CircuitState.HALF_OPEN
                    self.success_count = 0
                else:
                    raise CircuitBreakerOpenError("Circuit breaker is open")

        try:
            result = await func(*args, **kwargs)
            await self._on_success()
            return result
        except Exception as e:
            await self._on_failure()
            raise

    async def _on_success(self):
        """Handle successful call"""
        async with self.lock:
            if self.state == CircuitState.HALF_OPEN:
                self.success_count += 1
                if self.success_count >= self.success_threshold:
                    self.state = CircuitState.CLOSED
                    self.failure_count = 0
            elif self.state == CircuitState.CLOSED:
                self.failure_count = 0

    async def _on_failure(self):
        """Handle failed call"""
        async with self.lock:
            self.failure_count += 1
            self.last_failure_time = time.time()

            if self.failure_count >= self.failure_threshold:
                self.state = CircuitState.OPEN
            elif self.state == CircuitState.HALF_OPEN:
                self.state = CircuitState.OPEN

Per-Service Circuit Breaking

Multi-Service Circuit Breaker:

# circuit_breaker/multi_service.py
class MultiServiceCircuitBreaker:
    def __init__(self):
        self.breakers = {}

    def get_breaker(self, service_name: str) -> CircuitBreaker:
        """Get circuit breaker for service"""
        if service_name not in self.breakers:
            self.breakers[service_name] = CircuitBreaker()
        return self.breakers[service_name]

    async def call_service(self, service_name: str, func, *args, **kwargs):
        """Call service with circuit breaker"""
        breaker = self.get_breaker(service_name)
        return await breaker.call(func, *args, **kwargs)

DB Circuit Breaking

Postgres Circuit Breaker:

# circuit_breaker/db_circuit_breaker.py
class PostgresCircuitBreaker:
    def __init__(self, pool: asyncpg.Pool):
        self.pool = pool
        self.breaker = CircuitBreaker(
            failure_threshold=5,
            timeout=30.0
        )

    async def execute(self, query: str, *args):
        """Execute query with circuit breaker"""
        async def _execute():
            async with self.pool.acquire() as conn:
                return await conn.execute(query, *args)

        return await self.breaker.call(_execute)

ML Inference Circuit Breaking

ONNX Circuit Breaker:

# circuit_breaker/ml_circuit_breaker.py
class MLInferenceCircuitBreaker:
    def __init__(self, model_session: ort.InferenceSession):
        self.session = model_session
        self.breaker = CircuitBreaker(
            failure_threshold=3,
            timeout=60.0
        )

    async def infer(self, input_data: np.ndarray) -> np.ndarray:
        """Run inference with circuit breaker"""
        async def _infer():
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(
                None,
                lambda: self.session.run(None, {"input": input_data})
            )

        return await self.breaker.call(_infer)

Redis Failure Fallback

Pattern: Fallback to database when Redis fails.

# circuit_breaker/redis_fallback.py
class RedisWithFallback:
    def __init__(self, redis_client: redis.Redis, db_client):
        self.redis = redis_client
        self.db = db_client
        self.breaker = CircuitBreaker(failure_threshold=3, timeout=30.0)

    async def get(self, key: str):
        """Get value with Redis fallback to DB"""
        async def _get_from_redis():
            return await self.redis.get(key)

        try:
            return await self.breaker.call(_get_from_redis)
        except CircuitBreakerOpenError:
            # Fallback to database
            return await self.db.get(key)

NGINX Circuit Breaking via Upstream Health Checks

NGINX Configuration:

# nginx/circuit_breaker.conf
upstream backend {
    server backend1:8000 max_fails=3 fail_timeout=30s;
    server backend2:8000 max_fails=3 fail_timeout=30s backup;

    # Health check
    health_check interval=10s fails=3 passes=2;
}

server {
    location /api/ {
        proxy_pass http://backend;

        # Circuit breaker behavior
        proxy_next_upstream error timeout http_500 http_502 http_503;
        proxy_next_upstream_tries 3;
        proxy_next_upstream_timeout 10s;
    }
}

Bulkheads / Isolation

Workload Isolation

Pattern: Isolate workloads to prevent cascading failures.

# bulkheads/workload_isolation.py
class WorkloadIsolator:
    def __init__(self):
        self.pools = {
            "user_facing": asyncio.Semaphore(10),
            "etl": asyncio.Semaphore(5),
            "ml": asyncio.Semaphore(2),
            "gis": asyncio.Semaphore(4)
        }

    async def execute(self, workload_type: str, coro):
        """Execute workload in isolated pool"""
        semaphore = self.pools.get(workload_type)
        if not semaphore:
            raise ValueError(f"Unknown workload type: {workload_type}")

        async with semaphore:
            return await coro

Query Isolation in Postgres

Postgres Resource Groups:

-- Create resource groups
CREATE RESOURCE GROUP user_queries WITH (
    cpu_rate_limit = 50,
    memory_limit = 50
);

CREATE RESOURCE GROUP etl_queries WITH (
    cpu_rate_limit = 30,
    memory_limit = 30
);

-- Assign roles to resource groups
ALTER ROLE app_user SET resource_group = user_queries;
ALTER ROLE etl_user SET resource_group = etl_queries;

Connection Pool Isolation:

# bulkheads/postgres_isolation.py
class IsolatedPostgresPools:
    def __init__(self, dsn: str):
        self.pools = {
            "user_facing": None,
            "etl": None,
            "reporting": None
        }
        self.dsn = dsn

    async def connect(self):
        """Create isolated connection pools"""
        self.pools["user_facing"] = await asyncpg.create_pool(
            self.dsn,
            min_size=10,
            max_size=20,
            command_timeout=5.0
        )

        self.pools["etl"] = await asyncpg.create_pool(
            self.dsn,
            min_size=5,
            max_size=10,
            command_timeout=300.0
        )

        self.pools["reporting"] = await asyncpg.create_pool(
            self.dsn,
            min_size=2,
            max_size=5,
            command_timeout=60.0
        )

    async def execute(self, pool_name: str, query: str, *args):
        """Execute query in isolated pool"""
        pool = self.pools.get(pool_name)
        if not pool:
            raise ValueError(f"Unknown pool: {pool_name}")

        async with pool.acquire() as conn:
            return await conn.execute(query, *args)

Separate Redis Pools

Redis Connection Pool Isolation:

# bulkheads/redis_isolation.py
class IsolatedRedisPools:
    def __init__(self):
        self.pools = {
            "cache": redis.ConnectionPool(host="redis", db=0, max_connections=50),
            "sessions": redis.ConnectionPool(host="redis", db=1, max_connections=20),
            "streams": redis.ConnectionPool(host="redis", db=2, max_connections=10)
        }

    def get_client(self, pool_name: str) -> redis.Redis:
        """Get Redis client for isolated pool"""
        pool = self.pools.get(pool_name)
        if not pool:
            raise ValueError(f"Unknown pool: {pool_name}")

        return redis.Redis(connection_pool=pool)

Separate ONNX Inference Worker Pools

ML Worker Pool Isolation:

# bulkheads/ml_isolation.py
class IsolatedMLPools:
    def __init__(self):
        self.pools = {
            "real_time": AsyncWorkerPool(num_workers=4, queue_size=100),
            "batch": AsyncWorkerPool(num_workers=2, queue_size=1000),
            "experimental": AsyncWorkerPool(num_workers=1, queue_size=50)
        }

    async def infer(self, pool_name: str, input_data: np.ndarray) -> np.ndarray:
        """Run inference in isolated pool"""
        pool = self.pools.get(pool_name)
        if not pool:
            raise ValueError(f"Unknown pool: {pool_name}")

        return await pool.submit(lambda: model.infer(input_data))

Latency SLOs & Error Budgets

Define Latency SLOs

SLO Definitions:

# slos/latency_slos.yaml
slos:
  api:
    p50: 50ms
    p95: 200ms
    p99: 500ms
    availability: 99.9%

  postgres:
    p50: 10ms
    p95: 100ms
    p99: 500ms
    availability: 99.95%

  ml_inference:
    p50: 50ms
    p95: 200ms
    p99: 500ms
    availability: 99.5%

  gis_isochrone:
    p50: 1s
    p95: 5s
    p99: 10s
    availability: 99.0%

Prometheus SLO Tracking:

# slos/slo_tracking.py
from prometheus_client import Histogram, Counter

# Latency histograms
api_latency = Histogram(
    'api_request_duration_seconds',
    'API request latency',
    buckets=[0.01, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0]
)

# Error budget
error_budget = Counter(
    'slo_error_budget_consumed',
    'SLO error budget consumed',
    ['service', 'slo_type']
)

def track_api_latency(duration: float):
    """Track API latency"""
    api_latency.observe(duration)

    # Check SLO violation
    if duration > 0.5:  # P99 SLO: 500ms
        error_budget.labels(service='api', slo_type='latency_p99').inc()

PromQL SLO Queries:

# API P95 latency
histogram_quantile(0.95, rate(api_request_duration_seconds_bucket[5m]))

# SLO violation rate
rate(api_request_duration_seconds_bucket{le="0.2"}[5m]) / 
rate(api_request_duration_seconds_count[5m])

# Error budget remaining
1 - (rate(slo_error_budget_consumed[1h]) / 3600)

Track SLOs via Prometheus/Grafana

Grafana Dashboard:

{
  "dashboard": {
    "panels": [
      {
        "title": "API Latency SLO",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(api_request_duration_seconds_bucket[5m]))",
            "legendFormat": "P95 Latency"
          }
        ],
        "thresholds": [
          {
            "value": 0.2,
            "colorMode": "critical",
            "fill": true
          }
        ]
      }
    ]
  }
}

Autoscaling Strategies

HPA/VPA for RKE2

Horizontal Pod Autoscaler:

# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15

Vertical Pod Autoscaler:

# k8s/vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  updatePolicy:
    updateMode: "Auto"
  resourcePolicy:
    containerPolicies:
      - containerName: api
        minAllowed:
          cpu: 100m
          memory: 128Mi
        maxAllowed:
          cpu: 2
          memory: 4Gi

CPU/GPU Autoscaling for Inference Workers

GPU Autoscaling:

# k8s/gpu-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ml-inference-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ml-inference
  minReplicas: 1
  maxReplicas: 5
  metrics:
    - type: Pods
      pods:
        metric:
          name: gpu_utilization
        target:
          type: AverageValue
          averageValue: "70"

Autoscaling Postgres Read Replicas

Pattern: Scale read replicas based on query load.

# autoscaling/postgres_replicas.py
class PostgresReplicaAutoscaler:
    def __init__(self, k8s_client, deployment_name: str):
        self.k8s = k8s_client
        self.deployment_name = deployment_name

    async def scale_based_on_load(self):
        """Scale read replicas based on query load"""
        # Get current metrics
        query_rate = await get_prometheus_metric(
            "rate(postgres_queries_total[5m])"
        )

        # Calculate desired replicas
        target_query_rate_per_replica = 1000  # queries/sec
        desired_replicas = max(
            1,
            int(query_rate / target_query_rate_per_replica)
        )

        # Scale deployment
        await self.k8s.scale_deployment(
            self.deployment_name,
            replicas=desired_replicas
        )

Autoscaling Redis Clusters

Redis Cluster Autoscaling:

# k8s/redis-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: redis-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: redis
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Autoscale-on-Queue-Depth

Redis Streams Autoscaling:

# autoscaling/queue_depth.py
class QueueDepthAutoscaler:
    def __init__(self, k8s_client, deployment_name: str):
        self.k8s = k8s_client
        self.deployment_name = deployment_name

    async def scale_based_on_queue(self):
        """Scale based on queue depth"""
        # Get queue depth
        queue_depth = await get_redis_stream_length("jobs:queue")

        # Calculate desired replicas (1 worker per 100 queued jobs)
        jobs_per_worker = 100
        desired_replicas = max(
            1,
            int(queue_depth / jobs_per_worker)
        )

        # Scale deployment
        await self.k8s.scale_deployment(
            self.deployment_name,
            replicas=desired_replicas
        )

Graceful Degradation Patterns

Serving Stale Cached Responses

Pattern: Return stale cache when service is down.

# graceful_degradation/stale_cache.py
class StaleCacheDegradation:
    def __init__(self, cache_client: redis.Redis, max_staleness: int = 3600):
        self.cache = cache_client
        self.max_staleness = max_staleness

    async def get_with_stale_fallback(self, key: str, fetch_func: callable):
        """Get value, return stale cache if fetch fails"""
        try:
            # Try fresh fetch
            return await fetch_func()
        except Exception:
            # Fallback to stale cache
            cached = self.cache.get(key)
            if cached:
                data = json.loads(cached)
                age = time.time() - data.get("timestamp", 0)

                if age < self.max_staleness:
                    return data["value"]

            raise

Lower-Resolution Tiles

Pattern: Return lower zoom tiles when under load.

# graceful_degradation/tile_degradation.py
class TileDegradation:
    async def get_tile(self, z: int, x: int, y: int):
        """Get tile, degrade resolution if under load"""
        current_load = await get_system_load()

        if current_load > 0.9:
            # Return lower zoom tile
            if z > 10:
                return await self.get_tile(z - 1, x // 2, y // 2)

        return await generate_tile(z, x, y)

Approximate Results

Pattern: Return approximate results when exact computation is too slow.

# graceful_degradation/approximate.py
class ApproximateDegradation:
    async def compute_result(self, query: str, exact: bool = True) -> dict:
        """Compute result, return approximate if needed"""
        start_time = time.time()
        timeout = 5.0

        try:
            if exact:
                result = await asyncio.wait_for(
                    compute_exact(query),
                    timeout=timeout
                )
            else:
                result = await compute_approximate(query)

            return result
        except asyncio.TimeoutError:
            # Fallback to approximate
            return await compute_approximate(query)

Reduced ML Inference Precision

Pattern: Use quantized models when under load.

# graceful_degradation/ml_precision.py
class MLPrecisionDegradation:
    def __init__(self):
        self.full_model = load_model("models/full.onnx")
        self.quantized_model = load_model("models/quantized.onnx")

    async def infer(self, input_data: np.ndarray, current_load: float):
        """Run inference, use quantized model if under load"""
        if current_load > 0.9:
            # Use quantized model (faster, lower precision)
            return await self.quantized_model.infer(input_data)
        else:
            # Use full precision model
            return await self.full_model.infer(input_data)

GIS-Specific Resilience Patterns

Caching Geospatial Queries

Pattern: Cache expensive spatial queries.

# gis_resilience/cache_geo_queries.py
class GeospatialQueryCache:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    async def execute_cached(self, query: str, params: dict) -> list:
        """Execute geospatial query with caching"""
        cache_key = f"gis:query:{hashlib.sha256(json.dumps(params).encode()).hexdigest()}"

        # Check cache
        cached = self.redis.get(cache_key)
        if cached:
            return json.loads(cached)

        # Execute query
        result = await execute_spatial_query(query, params)

        # Cache result
        self.redis.setex(cache_key, 3600, json.dumps(result))

        return result

Precomputing Tiles as Fallback

Pattern: Precompute tiles for common areas.

# gis_resilience/precompute_tiles.py
class TilePrecomputation:
    async def precompute_common_tiles(self, bbox: dict, zoom_levels: list):
        """Precompute tiles for common areas"""
        for z in zoom_levels:
            for x, y in generate_tile_coords(bbox, z):
                tile = await generate_tile(z, x, y)
                cache_key = f"gis:tile:{z}:{x}:{y}"
                self.redis.setex(cache_key, 604800, tile.to_bytes())

Limiting Heavy ST_* Geoprocessing Functions

Pattern: Limit concurrent spatial operations.

# gis_resilience/limit_geoprocessing.py
class GeoprocessingLimiter:
    def __init__(self, max_concurrent: int = 4):
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def st_intersects(self, geom1, geom2):
        """ST_Intersects with concurrency limit"""
        async with self.semaphore:
            return await execute_st_intersects(geom1, geom2)

Queuing Isochrone Requests

Pattern: Queue isochrone requests to prevent overload.

# gis_resilience/isochrone_queue.py
class IsochroneQueue:
    def __init__(self, max_queue: int = 100):
        self.queue = asyncio.Queue(maxsize=max_queue)
        self.worker_pool = AsyncWorkerPool(num_workers=2, queue_size=max_queue)

    async def compute_isochrone(self, point: dict, max_time: int) -> dict:
        """Compute isochrone with queuing"""
        future = asyncio.Future()

        try:
            await asyncio.wait_for(
                self.queue.put((point, max_time, future)),
                timeout=0.1
            )
            return await future
        except asyncio.TimeoutError:
            raise QueueFullError("Isochrone queue full")

ML-Specific Resilience Patterns

Concurrency-Safe ONNX Inference

Pattern: Use thread-safe ONNX session.

# ml_resilience/onnx_concurrency.py
import threading

class ThreadSafeONNXInference:
    def __init__(self, model_path: str):
        self.session = ort.InferenceSession(model_path)
        self.lock = threading.Lock()

    def infer(self, input_data: np.ndarray) -> np.ndarray:
        """Thread-safe ONNX inference"""
        with self.lock:
            return self.session.run(None, {"input": input_data})

Preloading Models into Memory

Pattern: Preload models to avoid cold starts.

# ml_resilience/model_preload.py
class ModelPreloader:
    def __init__(self, model_paths: list):
        self.models = {}
        self.loading = {}

    async def preload_models(self):
        """Preload all models"""
        for model_id, path in model_paths.items():
            asyncio.create_task(self._load_model(model_id, path))

    async def _load_model(self, model_id: str, path: str):
        """Load model asynchronously"""
        self.loading[model_id] = True
        try:
            self.models[model_id] = ort.InferenceSession(path)
        finally:
            self.loading[model_id] = False

Hot-Path ML Fallback Models

Pattern: Use smaller models when under load.

# ml_resilience/fallback_models.py
class MLFallbackModels:
    def __init__(self):
        self.full_model = load_model("models/full.onnx")
        self.fallback_model = load_model("models/fallback.onnx")

    async def infer(self, input_data: np.ndarray, current_load: float):
        """Run inference with fallback model if needed"""
        if current_load > 0.9:
            return await self.fallback_model.infer(input_data)
        return await self.full_model.infer(input_data)

Architecture Diagrams

Concurrency Control Reference

graph TB
    subgraph Clients["Clients"]
        C1["Client 1"]
        C2["Client 2"]
        C3["Client N"]
    end

    subgraph RateLimit["Rate Limiter"]
        TB["Token Bucket<br/>(100 req/s)"]
    end

    subgraph Concurrency["Concurrency Control"]
        Sem["Semaphore<br/>(Max 10)"]
        Queue["Queue<br/>(Max 100)"]
    end

    subgraph Workers["Worker Pools"]
        WP1["User-Facing Pool<br/>(10 workers)"]
        WP2["ETL Pool<br/>(5 workers)"]
        WP3["ML Pool<br/>(2 workers)"]
    end

    subgraph Services["Services"]
        API["API Service"]
        PG["Postgres<br/>(Pool: 20)"]
        ML["ML Service"]
    end

    C1 & C2 & C3 --> TB
    TB --> Sem
    Sem --> Queue
    Queue --> WP1 & WP2 & WP3
    WP1 --> API
    WP2 --> PG
    WP3 --> ML

Rate Limit Gateway

graph LR
    Client["Client"] --> NGINX["NGINX<br/>(Rate Limit Zone)"]
    NGINX -->|"10 req/s"| API["API Service"]
    API -->|"Per-User Limit"| Redis["Redis<br/>(Token Bucket)"]
    Redis -->|"Allowed"| Backend["Backend Services"]
    Redis -->|"Rate Limited"| Reject["429 Too Many Requests"]

Observability & Telemetry

PromQL Queries

# Queue depth
rate(queue_size[5m])

# Request latency percentiles
histogram_quantile(0.95, rate(request_duration_seconds_bucket[5m]))

# Circuit breaker state
circuit_breaker_state{state="open"}

# Rate limit hits
rate(rate_limit_hits_total[5m])

# Backpressure rejections
rate(backpressure_rejections_total[5m])

Anti-Patterns

Unlimited Concurrency

Symptom: System overwhelmed, OOM errors, service crashes.

Fix: Always set concurrency limits (semaphores, worker pools).

No Timeout or Retry Limits

Symptom: Requests hang indefinitely, resources exhausted.

Fix: Set timeouts on all operations, limit retries with exponential backoff.

Retry Storms Meltdown

Symptom: Cascading failures from unlimited retries.

Fix: Use circuit breakers, exponential backoff, jitter, retry budgets.

DB Saturated Due to "Pool=All" Connections

Symptom: Database connection exhaustion, all requests fail.

Fix: Use connection pools with limits, monitor pool usage.

ML Inference Workers Blocking for GPU Warmup

Symptom: All workers blocked waiting for GPU, queue builds up.

Fix: Preload models, use separate warmup workers, graceful degradation.

GIS Queries Running Unbounded

Symptom: Expensive spatial queries block all requests.

Fix: Limit concurrent spatial operations, queue expensive queries, use timeouts.

Redis-Backed Rate Limiting with No TTLs

Symptom: Rate limit keys never expire, memory leak.

Fix: Always set TTL on rate limit keys, use sliding windows.

Using sleep() Instead of Proper Backpressure

Symptom: Inefficient resource usage, poor responsiveness.

Fix: Use semaphores, queues, proper async/await patterns.

Running All Workloads on Single Worker Class

Symptom: One workload type blocks all others.

Fix: Use bulkheads, separate worker pools per workload type.

No Circuit Breaking on Remote Services

Symptom: Cascading failures when remote service is down.

Fix: Implement circuit breakers for all remote service calls.

Infinite Queue Growth

Symptom: Memory exhaustion from unbounded queues.

Fix: Set queue size limits, implement backpressure, reject when full.

Coupling Retries Across Microservices

Symptom: Retry storms cascade across services.

Fix: Use circuit breakers, limit retries, use exponential backoff with jitter.

Final Checklists

Rate Limiting Readiness

  • Rate limits defined for all endpoints
  • Per-user, per-IP, per-org limits configured
  • Redis-based rate limiting with TTLs
  • NGINX rate limits configured
  • Rate limit monitoring in place
  • Rate limit error handling implemented

Concurrency Safety

  • Concurrency limits set for all pools
  • CPU-bound vs IO-bound workloads separated
  • Worker pools sized appropriately
  • Semaphores used for limiting
  • Thread safety verified
  • Deadlock prevention measures

Backpressure Correctness

  • Queue size limits set
  • Backpressure signals implemented
  • Rejection handling in place
  • Queue depth monitored
  • Backpressure propagated upstream

Load Shedding

  • Priority levels defined
  • Shedding thresholds configured
  • Low-priority traffic identified
  • Graceful degradation implemented
  • Shedding metrics tracked

Circuit Breaking

  • Circuit breakers for all remote calls
  • Failure thresholds configured
  • Timeout values set
  • Half-open state handling
  • Fallback behaviors defined

Autoscaling

  • HPA configured for services
  • Metrics-based scaling rules
  • Queue-depth scaling implemented
  • Scale-down policies configured
  • Resource limits set

Observability Coverage

  • Latency percentiles tracked
  • Queue depths monitored
  • Circuit breaker states exposed
  • Rate limit hits logged
  • Error budgets tracked
  • SLO dashboards configured

See Also


This guide provides a complete framework for system resilience. Start with rate limiting and concurrency control, then add circuit breakers and backpressure. The goal is graceful degradation under load, not perfect availability.