summaryrefslogtreecommitdiffstats
path: root/internal/ratelimit/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'internal/ratelimit/README.md')
-rw-r--r--internal/ratelimit/README.md341
1 files changed, 341 insertions, 0 deletions
diff --git a/internal/ratelimit/README.md b/internal/ratelimit/README.md
new file mode 100644
index 0000000..a7f248d
--- /dev/null
+++ b/internal/ratelimit/README.md
@@ -0,0 +1,341 @@
1# Rate Limiting
2
3This package provides per-user rate limiting for gRPC endpoints using the token bucket algorithm.
4
5## Overview
6
7Rate limiting prevents abuse and ensures fair resource allocation across users. This implementation:
8
9- **Per-user quotas**: Different limits for each authenticated pubkey
10- **IP-based fallback**: Rate limit unauthenticated requests by IP address
11- **Method-specific limits**: Different quotas for different operations (e.g., stricter limits for PublishEvent)
12- **Token bucket algorithm**: Allows bursts while maintaining average rate
13- **Standard gRPC errors**: Returns `ResourceExhausted` (HTTP 429) when limits exceeded
14
15## How It Works
16
17### Token Bucket Algorithm
18
19Each user (identified by pubkey or IP) has a "bucket" of tokens:
20
211. **Tokens refill** at a configured rate (e.g., 10 requests/second)
222. **Each request consumes** one token
233. **Bursts allowed** up to bucket capacity (e.g., 20 tokens)
244. **Requests blocked** when bucket is empty
25
26Example with 10 req/s limit and 20 token burst:
27```
28Time 0s: User makes 20 requests → All succeed (burst)
29Time 0s: User makes 21st request → Rejected (bucket empty)
30Time 1s: Bucket refills by 10 tokens
31Time 1s: User makes 10 requests → All succeed
32```
33
34### Integration with Authentication
35
36Rate limiting works seamlessly with the auth package:
37
381. **Authenticated users** (via NIP-98): Rate limited by pubkey
392. **Unauthenticated users**: Rate limited by IP address
403. **Auth interceptor runs first**, making pubkey available to rate limiter
41
42## Usage
43
44### Basic Setup
45
46```go
47import (
48 "northwest.io/muxstr/internal/auth"
49 "northwest.io/muxstr/internal/ratelimit"
50 "google.golang.org/grpc"
51)
52
53// Configure rate limiter
54limiter := ratelimit.New(&ratelimit.Config{
55 // Default: 10 requests/second per user, burst of 20
56 RequestsPerSecond: 10,
57 BurstSize: 20,
58
59 // Unauthenticated users: 5 requests/second per IP
60 IPRequestsPerSecond: 5,
61 IPBurstSize: 10,
62})
63
64// Create server with auth + rate limit interceptors
65server := grpc.NewServer(
66 grpc.ChainUnaryInterceptor(
67 auth.NostrUnaryInterceptor(authOpts), // Auth runs first
68 ratelimit.UnaryInterceptor(limiter), // Rate limit runs second
69 ),
70 grpc.ChainStreamInterceptor(
71 auth.NostrStreamInterceptor(authOpts),
72 ratelimit.StreamInterceptor(limiter),
73 ),
74)
75```
76
77### Method-Specific Limits
78
79Different operations can have different rate limits:
80
81```go
82limiter := ratelimit.New(&ratelimit.Config{
83 // Default for all methods
84 RequestsPerSecond: 10,
85 BurstSize: 20,
86
87 // Override for specific methods
88 MethodLimits: map[string]ratelimit.MethodLimit{
89 "/nostr.v1.NostrRelay/PublishEvent": {
90 RequestsPerSecond: 2, // Stricter: only 2 publishes/sec
91 BurstSize: 5,
92 },
93 "/nostr.v1.NostrRelay/Subscribe": {
94 RequestsPerSecond: 1, // Only 1 new subscription/sec
95 BurstSize: 3,
96 },
97 "/nostr.v1.NostrRelay/QueryEvents": {
98 RequestsPerSecond: 20, // More lenient: 20 queries/sec
99 BurstSize: 50,
100 },
101 },
102})
103```
104
105### Per-User Custom Limits
106
107Set different limits for specific users:
108
109```go
110limiter := ratelimit.New(&ratelimit.Config{
111 RequestsPerSecond: 10,
112 BurstSize: 20,
113
114 // VIP users get higher limits
115 UserLimits: map[string]ratelimit.UserLimit{
116 "vip-pubkey-abc123": {
117 RequestsPerSecond: 100,
118 BurstSize: 200,
119 },
120 "premium-pubkey-def456": {
121 RequestsPerSecond: 50,
122 BurstSize: 100,
123 },
124 },
125})
126```
127
128### Disable Rate Limiting for Specific Methods
129
130```go
131limiter := ratelimit.New(&ratelimit.Config{
132 RequestsPerSecond: 10,
133 BurstSize: 20,
134
135 // Don't rate limit these methods
136 SkipMethods: []string{
137 "/grpc.health.v1.Health/Check",
138 },
139})
140```
141
142## Configuration Reference
143
144### Config
145
146- **`RequestsPerSecond`**: Default rate limit (tokens per second)
147- **`BurstSize`**: Maximum burst size (bucket capacity)
148- **`IPRequestsPerSecond`**: Rate limit for unauthenticated users (per IP)
149- **`IPBurstSize`**: Burst size for IP-based limits
150- **`MethodLimits`**: Map of method-specific overrides
151- **`UserLimits`**: Map of per-user custom limits (by pubkey)
152- **`SkipMethods`**: Methods that bypass rate limiting
153- **`CleanupInterval`**: How often to remove idle limiters (default: 5 minutes)
154
155### MethodLimit
156
157- **`RequestsPerSecond`**: Rate limit for this method
158- **`BurstSize`**: Burst size for this method
159
160### UserLimit
161
162- **`RequestsPerSecond`**: Rate limit for this user
163- **`BurstSize`**: Burst size for this user
164- **`MethodLimits`**: Optional method overrides for this user
165
166## Error Handling
167
168When rate limit is exceeded, the interceptor returns:
169
170```
171Code: ResourceExhausted (HTTP 429)
172Message: "rate limit exceeded for <pubkey/IP>"
173```
174
175Clients should implement exponential backoff:
176
177```go
178for {
179 resp, err := client.PublishEvent(ctx, req)
180 if err != nil {
181 if status.Code(err) == codes.ResourceExhausted {
182 // Rate limited - wait and retry
183 time.Sleep(backoff)
184 backoff *= 2
185 continue
186 }
187 return err
188 }
189 return resp, nil
190}
191```
192
193## Monitoring
194
195The rate limiter tracks:
196
197- **Active limiters**: Number of users being tracked
198- **Requests allowed**: Total requests that passed
199- **Requests denied**: Total requests that were rate limited
200
201Access stats:
202
203```go
204stats := limiter.Stats()
205fmt.Printf("Active users: %d\n", stats.ActiveLimiters)
206fmt.Printf("Allowed: %d, Denied: %d\n", stats.Allowed, stats.Denied)
207fmt.Printf("Denial rate: %.2f%%\n", stats.DenialRate())
208```
209
210## Performance Considerations
211
212### Memory Usage
213
214Each tracked user (pubkey or IP) consumes ~200 bytes. With 10,000 active users:
215- Memory: ~2 MB
216- Lookup: O(1) with sync.RWMutex
217
218Idle limiters are cleaned up periodically (default: every 5 minutes).
219
220### Throughput
221
222Rate limiting adds minimal overhead:
223- Token check: ~100 nanoseconds
224- Lock contention: Read lock for lookups, write lock for new users only
225
226Benchmark results (on typical hardware):
227```
228BenchmarkRateLimitAllow-8 20000000 85 ns/op
229BenchmarkRateLimitDeny-8 20000000 82 ns/op
230```
231
232### Distributed Deployments
233
234This implementation is **in-memory** and works for single-instance deployments.
235
236For distributed deployments across multiple relay instances:
237
238**Option 1: Accept per-instance limits** (simplest)
239- Each instance tracks its own limits
240- Users get N × limit if they connect to N different instances
241- Usually acceptable for most use cases
242
243**Option 2: Shared Redis backend** (future enhancement)
244- Centralized rate limiting across all instances
245- Requires Redis dependency
246- Adds network latency (~1-2ms per request)
247
248**Option 3: Sticky sessions** (via load balancer)
249- Route users to the same instance
250- Per-instance limits become per-user limits
251- No coordination needed
252
253## Example: Relay with Tiered Access
254
255```go
256// Free tier: 10 req/s, strict publish limits
257// Premium tier: 50 req/s, relaxed limits
258// Admin tier: No limits
259
260func setupRateLimit() *ratelimit.Limiter {
261 return ratelimit.New(&ratelimit.Config{
262 // Free tier defaults
263 RequestsPerSecond: 10,
264 BurstSize: 20,
265
266 MethodLimits: map[string]ratelimit.MethodLimit{
267 "/nostr.v1.NostrRelay/PublishEvent": {
268 RequestsPerSecond: 2,
269 BurstSize: 5,
270 },
271 },
272
273 // Premium users
274 UserLimits: map[string]ratelimit.UserLimit{
275 "premium-user-1": {
276 RequestsPerSecond: 50,
277 BurstSize: 100,
278 },
279 },
280
281 // Admins bypass limits
282 SkipMethods: []string{},
283 SkipUsers: []string{
284 "admin-pubkey-abc",
285 },
286 })
287}
288```
289
290## Best Practices
291
2921. **Set conservative defaults**: Start with low limits and increase based on usage
2932. **Monitor denial rates**: High denial rates indicate limits are too strict
2943. **Method-specific tuning**: Writes (PublishEvent) should be stricter than reads
2954. **Burst allowance**: Set burst = 2-3× rate to handle legitimate traffic spikes
2965. **IP-based limits**: Set lower than authenticated limits to encourage auth
2976. **Cleanup interval**: Balance memory usage vs. repeated user setup overhead
298
299## Security Considerations
300
301### Rate Limit Bypass
302
303Rate limiting can be bypassed by:
304- Using multiple pubkeys (Sybil attack)
305- Using multiple IPs (distributed attack)
306
307Mitigations:
308- Require proof-of-work for new pubkeys
309- Monitor for suspicious patterns (many low-activity accounts)
310- Implement global rate limits in addition to per-user limits
311
312### DoS Protection
313
314Rate limiting helps with DoS but isn't sufficient alone:
315- Combine with connection limits
316- Implement request size limits
317- Use timeouts and deadlines
318- Consider L3/L4 DDoS protection (CloudFlare, etc.)
319
320## Integration with NIP-98 Auth
321
322Rate limiting works naturally with authentication:
323
324```
325Request flow:
3261. Request arrives
3272. Auth interceptor validates NIP-98 event → extracts pubkey
3283. Rate limit interceptor checks quota for pubkey
3294. If allowed → handler processes request
3305. If denied → return ResourceExhausted error
331```
332
333For unauthenticated requests:
334```
3351. Request arrives
3362. Auth interceptor allows (if Required: false)
3373. Rate limit interceptor uses IP address
3384. Check quota for IP → likely stricter limits
339```
340
341This encourages users to authenticate to get better rate limits!