Skip to main content

Rate Limiting and Load Shedding

Understanding rate limiting for preventing abuse and load shedding for maintaining service quality under overload.


Table of Contents​

  1. Rate Limiting
  2. Rate Limiting Algorithms
  3. Load Shedding
  4. Graceful Degradation
  5. Backpressure
  6. Implementation Strategies
  7. Production Considerations

Rate Limiting​

What Is Rate Limiting​

Rate limiting controls the rate of requests a client can make. It prevents:

  • Resource exhaustion from excessive requests
  • Abuse and denial of service attacks
  • Unfair resource allocation
  • Cascading failures from traffic spikes

Why Rate Limit​

Protect System Resources:

  • Prevent any single client from overwhelming system
  • Ensure resources available for all users
  • Maintain consistent performance

Cost Control:

  • Limit expensive operations
  • Prevent runaway costs from API usage
  • Enforce usage tiers

Fair Usage:

  • Ensure no user monopolizes resources
  • Provide baseline service level for all users

Security:

  • Slow down brute force attacks
  • Prevent credential stuffing
  • Mitigate DDoS attempts

Rate Limiting Dimensions​

Per User:

  • Limit requests per authenticated user
  • Most common for APIs

Per IP Address:

  • Limit requests per source IP
  • Good for anonymous/public endpoints
  • Beware: NAT and proxies share IPs

Per API Key:

  • Limit requests per API key
  • Standard for external APIs

Per Endpoint:

  • Different limits for different endpoints
  • Expensive operations get lower limits

Per Resource:

  • Limit operations on specific resources
  • Example: 5 login attempts per account per minute

Global:

  • Total requests across all clients
  • Prevents system-wide overload

Rate Limit Responses​

When limit exceeded:

HTTP 429 Too Many Requests:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1610000000

{
"error": "Rate limit exceeded",
"message": "You have exceeded 100 requests per minute. Please wait 60 seconds.",
"retryAfter": 60
}

Headers:

  • X-RateLimit-Limit: Maximum requests allowed
  • X-RateLimit-Remaining: Requests remaining in window
  • X-RateLimit-Reset: Time when limit resets (Unix timestamp)
  • Retry-After: Seconds to wait before retrying

Rate Limit Tiers​

Different tiers for different users:

Free Tier:

  • 100 requests/minute
  • 10,000 requests/day

Basic Tier:

  • 1,000 requests/minute
  • 100,000 requests/day

Premium Tier:

  • 10,000 requests/minute
  • 1,000,000 requests/day

Enterprise:

  • Custom limits
  • Dedicated resources

Dynamic Rate Limiting​

Adjust limits based on:

  • System load
  • User behavior
  • Time of day
  • Subscription tier

Example: During high traffic, reduce limits to maintain service quality. During low traffic, allow higher limits.


Rate Limiting Algorithms​

Fixed Window Counter​

Count requests in fixed time windows.

Algorithm:

window = floor(now / windowSize)
key = userId + ":" + window
count = increment(key)
if count > limit:
reject request

Example:

  • Limit: 100 requests per minute
  • Window: 10:00:00 - 10:01:00
  • Window: 10:01:00 - 10:02:00

Advantages:

  • Simple to implement
  • Memory efficient
  • Easy to understand

Disadvantages:

  • Boundary problem: 200 requests in 2 seconds across window boundary
    • 100 at 10:00:59
    • 100 at 10:01:00
  • Traffic spikes at window boundaries

Sliding Window Log​

Track timestamp of each request.

Algorithm:

key = userId
timestamps = getTimestamps(key)
removeOlderThan(now - windowSize)
if timestamps.length >= limit:
reject request
add(now)

Advantages:

  • No boundary problem
  • Accurate request rate
  • Smooth enforcement

Disadvantages:

  • Memory intensive (store all timestamps)
  • Expensive at high volume

Sliding Window Counter​

Hybrid of fixed window and sliding window.

Algorithm:

currentWindow = floor(now / windowSize)
previousWindow = currentWindow - 1

currentCount = getCount(currentWindow)
previousCount = getCount(previousWindow)

percentageOfPreviousWindow = (windowSize - (now % windowSize)) / windowSize
estimatedCount = previousCount * percentageOfPreviousWindow + currentCount

if estimatedCount >= limit:
reject request

Advantages:

  • Memory efficient (only two counters)
  • Mitigates boundary problem
  • Good approximation

Disadvantages:

  • Approximate, not exact
  • Slightly more complex

Token Bucket​

Bucket holds tokens. Tokens added at fixed rate. Requests consume tokens.

Algorithm:

tokensPerSecond = limit / windowSize
tokens = getBucketTokens()
timeSinceLastRefill = now - lastRefill

newTokens = timeSinceLastRefill * tokensPerSecond
tokens = min(tokens + newTokens, bucketCapacity)

if tokens >= 1:
tokens -= 1
allow request
else:
reject request

Advantages:

  • Handles bursts (up to bucket capacity)
  • Smooth long-term rate
  • Flexible

Disadvantages:

  • More complex
  • Requires floating-point arithmetic
  • Bucket capacity needs tuning

Bucket capacity determines burst tolerance:

  • Small bucket: strict rate, small bursts
  • Large bucket: allows larger bursts

Leaky Bucket​

Requests enter bucket. Requests leave at fixed rate.

Algorithm:

queue.add(request)
if queue.size > bucketCapacity:
reject request

processRequests at fixed rate

Advantages:

  • Smooth output rate
  • Simple conceptually

Disadvantages:

  • Adds latency (queuing)
  • Need queue management
  • Doesn't handle bursts well

Comparison​

Fixed Window: Simple, but boundary problem.

Sliding Log: Accurate, but memory intensive.

Sliding Counter: Good balance of accuracy and efficiency.

Token Bucket: Flexible, handles bursts, most common in production.

Leaky Bucket: Smooth rate, but adds latency.

Recommendation: Token bucket for most use cases. Sliding window counter for simplicity.


Load Shedding​

What Is Load Shedding​

Load shedding intentionally drops requests when system is overloaded. Better to serve some requests well than all requests poorly.

Why Shed Load​

Maintain Quality:

  • High load degrades performance for all users
  • Better to reject some requests than slow down all
  • Users prefer fast errors to slow success

Prevent Cascade:

  • Overload in one service cascades to dependencies
  • Shedding load prevents cascade
  • System remains partially functional

Cost Control:

  • Computing resources have limits
  • Rejecting requests is cheaper than processing them
  • Prevent infrastructure costs from spiking

When to Shed Load​

Trigger load shedding when:

  • CPU utilization > 80%
  • Memory utilization > 85%
  • Event loop lag > 100ms
  • Request queue depth > 1000
  • Response time p99 > 2x normal
  • Error rate > 1%

Don't wait for complete failure. Shed load proactively.

What to Shed​

Priority-Based:

  • Shed low-priority requests first
  • Maintain high-priority operations
  • Examples:
    • High: checkout, payments
    • Medium: product viewing
    • Low: recommendations, analytics

User-Based:

  • Shed free tier users before paid users
  • Maintain service for premium customers

Operation-Based:

  • Shed expensive operations (complex queries, reports)
  • Maintain cheap operations (cache reads)

Randomized:

  • Randomly shed percentage of requests
  • Simple but doesn't differentiate importance

Load Shedding Strategies​

Percentage-Based:

  • Shed 10% of requests at 70% capacity
  • Shed 50% of requests at 90% capacity
  • Shed 90% of requests at 95% capacity

Queue-Based:

  • Reject new requests when queue full
  • Process queued requests first
  • Simple and effective

Latency-Based:

  • Shed requests if processing would exceed timeout
  • Better to reject immediately than waste resources on doomed requests

Adaptive:

  • Adjust shedding rate based on observed impact
  • Increase shedding if latency still high
  • Decrease shedding as system recovers

Load Shedding Response​

Return clear errors for shed requests:

HTTP/1.1 503 Service Unavailable
Retry-After: 10

{
"error": "Service overloaded",
"message": "Service is currently experiencing high load. Please retry in 10 seconds.",
"retryAfter": 10
}

Admission Control​

Reject requests at the boundary before consuming resources:

API Gateway:

  • Check system health before routing
  • Reject if unhealthy
  • Prevents load from reaching backend

Load Balancer:

  • Health checks on backend services
  • Don't route to unhealthy instances

Service Entrance:

  • Check resource availability before processing
  • Reject if insufficient resources

Concurrency Limits​

Limit concurrent requests to prevent overload:

maxConcurrent = 1000
currentConcurrent = getConcurrentRequests()

if currentConcurrent >= maxConcurrent:
reject request with 503
else:
process request

This naturally sheds load when system at capacity.


Graceful Degradation​

What Is Graceful Degradation​

Gradually reduce functionality under stress instead of complete failure.

Full Functionality: All features available, optimal performance.

Degraded Mode: Some features disabled, acceptable performance.

Minimal Mode: Only critical features, degraded performance.

Complete Failure: Nothing works.

Goal: Stay in degraded mode, avoid complete failure.

Degradation Strategies​

Disable Non-Critical Features:

  • Disable recommendations under load
  • Disable reviews and ratings
  • Disable social features
  • Maintain core functionality (browsing, checkout)

Reduce Quality:

  • Lower image resolution
  • Simpler search algorithms
  • Fewer search results
  • Cached data instead of fresh

Increase Cache TTL:

  • Serve slightly stale data
  • Reduce database load
  • Users tolerate stale data better than unavailability

Async Processing:

  • Queue non-critical writes
  • Process later when load decreases
  • Confirm request accepted, process asynchronously

Feature Flags for Degradation​

Use feature flags to control degradation:

if systemLoad > 80%:
disableRecommendations()
disableReviews()
increaseCacheTTL()

if systemLoad > 90%:
disableSocialFeatures()
disableAnalytics()
serveStaticContentOnly()

if systemLoad < 70%:
enableAllFeatures()

Communicating Degradation​

Tell users about degraded service:

Banner:

"We're experiencing high traffic. Some features may be temporarily unavailable."

Feature-Specific:

"Recommendations temporarily unavailable. Browse our popular items."

Status Page:

Service Status: Degraded
- Core features: Operational
- Recommendations: Disabled
- Expected resolution: 15 minutes

Graceful vs Ungraceful Degradation​

Ungraceful:

  • Random features break
  • Inconsistent errors
  • Poor user experience
  • Confusing failure modes

Graceful:

  • Predictable degradation path
  • Clear error messages
  • Core functionality maintained
  • Users understand situation

Backpressure​

What Is Backpressure​

Backpressure signals upstream to slow down when downstream can't keep up.

Without Backpressure:

  • Upstream produces faster than downstream consumes
  • Queues grow unbounded
  • Memory exhaustion
  • System crash

With Backpressure:

  • Downstream signals "I'm full"
  • Upstream slows production
  • Queues remain bounded
  • System stable

Backpressure Mechanisms​

Explicit Signaling:

  • Downstream returns "slow down" signal
  • Upstream reduces rate

Blocking:

  • Downstream blocks when full
  • Upstream waits
  • Simple but can cause issues

Rate Adjustment:

  • Monitor queue depth
  • Increase rate if queue low
  • Decrease rate if queue high

Timeout-Based:

  • If operation takes too long, assume overload
  • Reduce rate

Queue-Based Backpressure​

Monitor queue depth:

if queueDepth < 10%:
normalRate()

if queueDepth > 50%:
reduceRate(50%)

if queueDepth > 90%:
reduceRate(90%)

if queueDepth == 100%:
rejectRequests()

TCP Backpressure​

TCP has built-in backpressure:

  • Receive window controls flow
  • If receiver's buffer full, window shrinks
  • Sender slows down automatically

HTTP/2 builds on this with flow control.

Stream Backpressure​

For stream processing:

  • Consumer signals when buffer full
  • Producer pauses production
  • Consumer signals when ready for more

Node.js streams implement this automatically.

Backpressure vs Load Shedding​

Backpressure: Signal upstream to slow down. Cooperative.

Load Shedding: Reject requests. Defensive.

Use both:

  • Backpressure for internal systems
  • Load shedding for external clients

Implementation Strategies​

Rate Limiting Implementation​

In-Memory:

  • Fast
  • Lost on restart
  • Per-instance only
  • Good for: low-traffic or non-critical limits

Redis:

  • Shared across instances
  • Persistent
  • Slightly higher latency
  • Good for: production systems

API Gateway:

  • Centralized enforcement
  • Built into many gateways (Kong, AWS API Gateway)
  • Good for: external APIs

Redis Rate Limiting​

Using Redis with Lua scripts for atomicity:

-- Token bucket in Redis
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

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

if tokens == nil then
tokens = capacity
last_refill = now
end

local elapsed = now - last_refill
local new_tokens = elapsed * rate
tokens = math.min(capacity, tokens + new_tokens)

if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return 1 -- allowed
else
return 0 -- rejected
end

Load Shedding Implementation​

Middleware:

function loadSheddingMiddleware(req, res, next) {
const load = getCurrentLoad()
const sheddingPercentage = calculateSheddingPercentage(load)

if (Math.random() < sheddingPercentage) {
return res.status(503).json({
error: 'Service overloaded',
retryAfter: 10
})
}

next()
}

Priority-Based:

function priorityLoadShedding(req, res, next) {
const priority = req.user?.tier || 'free'
const load = getCurrentLoad()

if (shouldShedLoad(priority, load)) {
return res.status(503).json({
error: 'Service overloaded',
retryAfter: 10
})
}

next()
}

function shouldShedLoad(priority, load) {
if (load < 70) return false
if (load > 95) return true

const thresholds = {
free: 70,
basic: 80,
premium: 90,
enterprise: 95
}

return load > thresholds[priority]
}

Concurrency Limiting​

Using semaphores:

const maxConcurrent = 1000
const semaphore = new Semaphore(maxConcurrent)

async function processRequest(req, res) {
const acquired = await semaphore.tryAcquire(timeout = 100ms)

if (!acquired) {
return res.status(503).json({
error: 'Too many concurrent requests'
})
}

try {
const result = await handleRequest(req)
res.json(result)
} finally {
semaphore.release()
}
}

Production Considerations​

Rate Limit Testing​

Test rate limiting behavior:

  • Verify limits enforced correctly
  • Verify headers returned correctly
  • Verify burst handling
  • Test near boundary conditions
  • Test distributed rate limiting consistency

Monitoring​

Track metrics:

  • Rate limit hits per endpoint
  • Rate limit hits per user
  • Percentage of requests rate limited
  • Load shedding percentage
  • Rejected request rate

Alert on:

  • High rate limit hit rate (may need limit adjustment)
  • Sustained load shedding (capacity issue)
  • Rate limiting errors (implementation bug)

Configuration​

Make limits configurable:

  • Per-environment limits
  • Per-user tier limits
  • Per-endpoint limits
  • Dynamic adjustment based on metrics

Don't hardcode limits in application code.

User Experience​

Communicate Limits:

  • Document limits in API docs
  • Return limit information in headers
  • Provide clear error messages

Progressive Enhancement:

  • Cache responses client-side
  • Implement client-side rate limiting
  • Batch requests where possible
  • Use webhooks instead of polling

Rate Limit Bypass​

Provide bypass mechanisms for:

  • Health checks (don't rate limit)
  • Internal services (different limits)
  • Emergency operations (bypass temporarily)

Use separate tokens or IP whitelisting.

Distributed Rate Limiting Challenges​

Clock Synchronization:

  • Servers may have slightly different times
  • Use centralized time source
  • Or accept minor inconsistency

Consistency:

  • Different instances may have different counts
  • Use centralized store (Redis) for consistency
  • Or accept eventual consistency

Performance:

  • Network latency to centralized store
  • Cache limits locally with sync
  • Balance consistency vs performance

Gradual Rollout​

When implementing load shedding:

Phase 1: Monitor

  • Track when load shedding would trigger
  • Don't actually shed load
  • Tune thresholds

Phase 2: Warn

  • Log when load is shed
  • Alert team
  • Continue serving requests

Phase 3: Engage

  • Actually shed load
  • Monitor user impact
  • Adjust thresholds based on feedback

Documentation​

Document for users:

  • Rate limits for each endpoint
  • How to handle rate limit errors
  • How to request limit increases
  • Burst allowances

Document for operations:

  • How load shedding works
  • When it triggers
  • How to adjust thresholds
  • Emergency bypass procedures