| title |
Add Rate Limiting to APIs |
| id |
api-rate-limiting |
| skillLevel |
intermediate |
| applicationPatternId |
building-apis |
| summary |
Protect your API from abuse by limiting request rates per client. |
| tags |
building-apis |
rate-limiting |
security |
performance |
|
| rule |
| description |
Use a rate limiter service to enforce request quotas per client. |
|
| author |
PaulJPhilp |
| related |
api-middleware |
api-authentication |
|
| lessonOrder |
1 |
Implement rate limiting as a service that tracks request counts and enforces limits per client (IP, API key, or user).
Rate limiting protects your API:
- Prevent abuse - Stop malicious flooding
- Fair usage - Share resources among clients
- Cost control - Limit expensive operations
- Stability - Prevent cascading failures
import { Effect, Context, Layer, Ref, HashMap, Data, Duration } from "effect"
import { HttpServerRequest, HttpServerResponse } from "@effect/platform"
// ============================================
// 1. Define rate limit types
// ============================================
interface RateLimitConfig {
readonly maxRequests: number
readonly windowMs: number
}
interface RateLimitState {
readonly count: number
readonly resetAt: number
}
class RateLimitExceededError extends Data.TaggedError("RateLimitExceededError")<{
readonly retryAfter: number
readonly limit: number
}> {}
// ============================================
// 2. Rate limiter service
// ============================================
interface RateLimiter {
readonly check: (key: string) => Effect.Effect<void, RateLimitExceededError>
readonly getStatus: (key: string) => Effect.Effect<{
remaining: number
resetAt: number
}>
}
class RateLimiterService extends Context.Tag("RateLimiter")<
RateLimiterService,
RateLimiter
>() {}
// ============================================
// 3. In-memory rate limiter implementation
// ============================================
const makeRateLimiter = (config: RateLimitConfig) =>
Effect.gen(function* () {
const state = yield* Ref.make(HashMap.empty<string, RateLimitState>())
const getOrCreateState = (key: string, now: number) =>
Ref.modify(state, (map) => {
const existing = HashMap.get(map, key)
if (existing._tag === "Some") {
// Check if window expired
if (now >= existing.value.resetAt) {
// Start new window
const newState: RateLimitState = {
count: 0,
resetAt: now + config.windowMs,
}
return [newState, HashMap.set(map, key, newState)]
}
return [existing.value, map]
}
// Create new entry
const newState: RateLimitState = {
count: 0,
resetAt: now + config.windowMs,
}
return [newState, HashMap.set(map, key, newState)]
})
const incrementCount = (key: string) =>
Ref.modify(state, (map) => {
const existing = HashMap.get(map, key)
if (existing._tag === "Some") {
const updated = { ...existing.value, count: existing.value.count + 1 }
return [updated.count, HashMap.set(map, key, updated)]
}
return [1, map]
})
const limiter: RateLimiter = {
check: (key) =>
Effect.gen(function* () {
const now = Date.now()
const currentState = yield* getOrCreateState(key, now)
if (currentState.count >= config.maxRequests) {
const retryAfter = Math.ceil((currentState.resetAt - now) / 1000)
return yield* Effect.fail(
new RateLimitExceededError({
retryAfter,
limit: config.maxRequests,
})
)
}
yield* incrementCount(key)
}),
getStatus: (key) =>
Effect.gen(function* () {
const now = Date.now()
const currentState = yield* getOrCreateState(key, now)
return {
remaining: Math.max(0, config.maxRequests - currentState.count),
resetAt: currentState.resetAt,
}
}),
}
return limiter
})
// ============================================
// 4. Rate limit middleware
// ============================================
const withRateLimit = <A, E, R>(
handler: Effect.Effect<A, E, R>
): Effect.Effect<
A | HttpServerResponse.HttpServerResponse,
E,
R | RateLimiterService | HttpServerRequest.HttpServerRequest
> =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const rateLimiter = yield* RateLimiterService
// Use IP address as key (in production, might use user ID or API key)
const clientKey = request.headers["x-forwarded-for"] || "unknown"
const result = yield* rateLimiter.check(clientKey).pipe(
Effect.matchEffect({
onFailure: (error) =>
Effect.succeed(
HttpServerResponse.json(
{
error: "Rate limit exceeded",
retryAfter: error.retryAfter,
},
{
status: 429,
headers: {
"Retry-After": String(error.retryAfter),
"X-RateLimit-Limit": String(error.limit),
"X-RateLimit-Remaining": "0",
},
}
)
),
onSuccess: () => handler,
})
)
return result
})
// ============================================
// 5. Usage example
// ============================================
const RateLimiterLive = Layer.effect(
RateLimiterService,
makeRateLimiter({
maxRequests: 100, // 100 requests
windowMs: 60 * 1000, // per minute
})
)
const apiEndpoint = withRateLimit(
Effect.gen(function* () {
// Your actual handler logic
return HttpServerResponse.json({ data: "Success!" })
})
)
| Strategy |
Key |
Best For |
| IP-based |
Client IP |
Public APIs |
| User-based |
User ID |
Authenticated APIs |
| API key |
API key |
B2B services |
| Endpoint |
IP + path |
Per-endpoint limits |
Response Headers
| Header |
Purpose |
X-RateLimit-Limit |
Max requests allowed |
X-RateLimit-Remaining |
Requests left |
X-RateLimit-Reset |
When window resets |
Retry-After |
Seconds until retry |
- Return 429 - Standard "Too Many Requests" status
- Include Retry-After - Tell clients when to retry
- Log violations - Track abuse patterns
- Tiered limits - Different limits for different tiers
- Graceful degradation - Consider "soft" limits first