Skip to content

Commit 5587285

Browse files
Sync docs from wheels@402afca
1 parent ecbb850 commit 5587285

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

docs/3.1.0/guides/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
* [Responding with Multiple Formats](handling-requests-with-controllers/responding-with-multiple-formats.md)
134134
* [Using the Flash](handling-requests-with-controllers/using-the-flash.md)
135135
* [Middleware](handling-requests-with-controllers/middleware.md)
136+
* [Rate Limiting](handling-requests-with-controllers/rate-limiting.md)
136137
* [Route Model Binding](handling-requests-with-controllers/route-model-binding.md)
137138
* [Using Filters](handling-requests-with-controllers/using-filters.md)
138139
* [Verification](handling-requests-with-controllers/verification.md)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Rate Limiting
2+
3+
Rate limiting protects your application from abuse by restricting how many requests a client can make within a time window. Wheels provides a built-in `RateLimiter` middleware with multiple strategies and storage backends.
4+
5+
## Quick Start
6+
7+
Add the rate limiter to your global middleware stack in `config/settings.cfm`:
8+
9+
```cfm
10+
set(middleware = [
11+
new wheels.middleware.RateLimiter(maxRequests=60, windowSeconds=60)
12+
]);
13+
```
14+
15+
This allows 60 requests per minute per client IP, using the fixed window strategy with in-memory storage.
16+
17+
## Configuration Reference
18+
19+
| Parameter | Default | Description |
20+
|-----------|---------|-------------|
21+
| `maxRequests` | `60` | Maximum requests allowed per window |
22+
| `windowSeconds` | `60` | Duration of the rate limit window in seconds |
23+
| `strategy` | `"fixedWindow"` | Algorithm: `"fixedWindow"`, `"slidingWindow"`, or `"tokenBucket"` |
24+
| `storage` | `"memory"` | Backend: `"memory"` or `"database"` |
25+
| `keyFunction` | `""` | Closure `function(request)` returning a string key. Defaults to client IP |
26+
| `headerPrefix` | `"X-RateLimit"` | Prefix for rate limit response headers |
27+
| `trustProxy` | `true` | Use `X-Forwarded-For` for client IP resolution |
28+
29+
## Strategies
30+
31+
### Fixed Window
32+
33+
Divides time into discrete buckets (e.g., 0:00–0:59, 1:00–1:59). Simple and memory-efficient.
34+
35+
```cfm
36+
new wheels.middleware.RateLimiter(strategy="fixedWindow")
37+
```
38+
39+
**Best for**: Most applications. Simple to understand and debug.
40+
41+
**Trade-off**: A client could send `maxRequests` at the end of one window and `maxRequests` at the start of the next, effectively doubling throughput at the boundary.
42+
43+
### Sliding Window
44+
45+
Maintains a log of request timestamps per client. More accurate than fixed window — the window slides with each request.
46+
47+
```cfm
48+
new wheels.middleware.RateLimiter(strategy="slidingWindow")
49+
```
50+
51+
**Best for**: Applications requiring precise rate enforcement without boundary spikes.
52+
53+
**Trade-off**: Uses more memory per client (stores individual timestamps).
54+
55+
### Token Bucket
56+
57+
Each client has a bucket that fills with tokens at a steady rate. Each request consumes one token. Allows controlled bursts up to `maxRequests`.
58+
59+
```cfm
60+
new wheels.middleware.RateLimiter(strategy="tokenBucket")
61+
```
62+
63+
**Best for**: APIs where you want to allow occasional bursts while maintaining a long-term average rate.
64+
65+
**Trade-off**: Slightly more complex state per client (token count + last refill time).
66+
67+
## Storage Backends
68+
69+
### In-Memory (Default)
70+
71+
Uses a `ConcurrentHashMap` for thread-safe storage. Stale entries are automatically cleaned up once per minute.
72+
73+
```cfm
74+
new wheels.middleware.RateLimiter(storage="memory")
75+
```
76+
77+
**Note**: Counters are lost on server restart and are not shared across multiple application servers. Suitable for single-server deployments or when approximate limiting is acceptable.
78+
79+
### Database
80+
81+
Stores rate limit data in a `wheels_rate_limits` table that is auto-created on first use.
82+
83+
```cfm
84+
new wheels.middleware.RateLimiter(storage="database")
85+
```
86+
87+
**Note**: Shared across all servers in a cluster. Adds a database query per request. Suitable for multi-server deployments where consistent limiting is required.
88+
89+
## Route-Scoped Rate Limiting
90+
91+
Apply different limits to different parts of your application using route middleware:
92+
93+
```cfm
94+
// config/routes.cfm
95+
mapper()
96+
// Strict limit on authentication endpoints.
97+
.scope(path="/auth", middleware=[
98+
new wheels.middleware.RateLimiter(maxRequests=10, windowSeconds=60)
99+
])
100+
.post(name="login", to="sessions##create")
101+
.post(name="register", to="users##create")
102+
.end()
103+
104+
// More generous limit on API endpoints.
105+
.scope(path="/api", middleware=[
106+
new wheels.middleware.RateLimiter(maxRequests=100, windowSeconds=60)
107+
])
108+
.resources("users")
109+
.resources("products")
110+
.end()
111+
.end();
112+
```
113+
114+
## Custom Key Functions
115+
116+
By default, the rate limiter identifies clients by IP address. You can customize this with a `keyFunction`:
117+
118+
```cfm
119+
// Rate limit by API key from request header.
120+
new wheels.middleware.RateLimiter(
121+
keyFunction=function(request) {
122+
if (StructKeyExists(request, "cgi") && StructKeyExists(request.cgi, "http_x_api_key")) {
123+
return "api:" & request.cgi.http_x_api_key;
124+
}
125+
return "anon:" & cgi.remote_addr;
126+
}
127+
)
128+
129+
// Rate limit by authenticated user ID.
130+
new wheels.middleware.RateLimiter(
131+
keyFunction=function(request) {
132+
if (StructKeyExists(session, "userId")) {
133+
return "user:" & session.userId;
134+
}
135+
return "anon:" & cgi.remote_addr;
136+
}
137+
)
138+
```
139+
140+
## Response Headers
141+
142+
Every response includes rate limit headers:
143+
144+
| Header | Description |
145+
|--------|-------------|
146+
| `X-RateLimit-Limit` | Maximum requests allowed per window |
147+
| `X-RateLimit-Remaining` | Requests remaining in the current window |
148+
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
149+
150+
When a request is rate limited, the response includes:
151+
152+
| Header | Description |
153+
|--------|-------------|
154+
| `Retry-After` | Seconds until the client can retry |
155+
156+
The status code is `429 Too Many Requests` with a plain text body.
157+
158+
The header prefix can be customized:
159+
160+
```cfm
161+
new wheels.middleware.RateLimiter(headerPrefix="X-MyApp-RateLimit")
162+
// Produces: X-MyApp-RateLimit-Limit, X-MyApp-RateLimit-Remaining, etc.
163+
```
164+
165+
## Multi-Server Deployments
166+
167+
With in-memory storage, each server maintains its own counters. If you have 3 servers behind a load balancer, a client could theoretically make `maxRequests × 3` total requests.
168+
169+
For consistent rate limiting across servers, use database storage:
170+
171+
```cfm
172+
new wheels.middleware.RateLimiter(
173+
storage="database",
174+
maxRequests=60,
175+
windowSeconds=60
176+
)
177+
```
178+
179+
The `wheels_rate_limits` table is auto-created on first use. No migration needed.
180+
181+
## Thread Safety
182+
183+
The in-memory implementation uses per-client locking (`cflock`) to ensure accurate counting under concurrent requests. If a lock cannot be acquired within 1 second (indicating extreme contention), the request is allowed through (fail-open). This prevents the rate limiter itself from becoming a bottleneck.

0 commit comments

Comments
 (0)