Rate Limiting
Sentinel provides multi-dimensional rate limiting with sliding window counters. You can enforce limits per IP address, per authenticated user, per route, and globally — all at the same time. Every dimension is evaluated independently, and a request must pass all applicable limits to be allowed through.
Opt-In Feature
Enabled: true in your RateLimitConfig to activate it. You only need to configure the dimensions you care about — any dimension left as nil is simply skipped.Enabling Rate Limiting
The simplest way to get started is to enable rate limiting with a single per-IP limit. This protects every route in your application from individual clients sending too many requests.
1import (2 "time"34 sentinel "github.com/MUKE-coder/sentinel"5 "github.com/gin-gonic/gin"6)78func main() {9 r := gin.Default()1011 sentinel.Mount(r, nil, sentinel.Config{12 RateLimit: sentinel.RateLimitConfig{13 Enabled: true,14 ByIP: &sentinel.Limit{Requests: 100, Window: time.Minute},15 },16 })1718 r.GET("/api/hello", func(c *gin.Context) {19 c.JSON(200, gin.H{"message": "Hello, World!"})20 })2122 r.Run(":8080")23}
Rate Limit Dimensions
Sentinel supports four independent rate limit dimensions. You can use any combination of them. Each dimension maintains its own set of counters and is evaluated in a specific order.
The Limit Struct
Every dimension is configured with the same Limit struct, which defines a maximum number of requests within a time window.
type Limit struct {Requests int // Maximum requests allowed within the windowWindow time.Duration // Time window (e.g., time.Minute, 15 * time.Minute)}
Per-IP (ByIP)
Each unique client IP address gets its own counter. This is the most common dimension and protects against individual clients overwhelming your server.
// 100 requests per minute per IP addressByIP: &sentinel.Limit{Requests: 100, Window: time.Minute}
Per-User (ByUser)
Each authenticated user gets their own counter, identified by a user ID string. This requires a UserIDExtractor function that extracts the user ID from the request. If the extractor returns an empty string (unauthenticated request), the per-user limit is skipped for that request.
// 500 requests per minute per authenticated userByUser: &sentinel.Limit{Requests: 500, Window: time.Minute},// Tell Sentinel how to identify the userUserIDExtractor: func(c *gin.Context) string {return c.GetHeader("X-User-ID")},
UserIDExtractor Required
ByUser limit is only enforced when UserIDExtractor is set. Without it, per-user rate limiting is silently skipped even if ByUser is configured.Per-Route (ByRoute)
Different routes can have different limits. The ByRoute map keys are exact route paths. Each route limit is tracked per IP address (the counter key is a combination of the route path and the client IP).
// Strict limits on sensitive endpointsByRoute: map[string]sentinel.Limit{"/api/login": {Requests: 5, Window: 15 * time.Minute},"/api/register": {Requests: 3, Window: time.Hour},"/api/password-reset": {Requests: 3, Window: time.Hour},},
Global
A single counter shared across all requests regardless of source. This is a safety net to protect your application from being overwhelmed by aggregate traffic.
// 5000 total requests per minute across all clientsGlobal: &sentinel.Limit{Requests: 5000, Window: time.Minute}
All Dimensions Together
1RateLimit: sentinel.RateLimitConfig{2 Enabled: true,3 Strategy: sentinel.SlidingWindow,45 // Per-IP: 100 req/min6 ByIP: &sentinel.Limit{Requests: 100, Window: time.Minute},78 // Per-user: 500 req/min (requires UserIDExtractor)9 ByUser: &sentinel.Limit{Requests: 500, Window: time.Minute},1011 // Per-route: different limits for sensitive endpoints12 ByRoute: map[string]sentinel.Limit{13 "/api/login": {Requests: 5, Window: 15 * time.Minute},14 "/api/register": {Requests: 3, Window: time.Hour},15 },1617 // Global: 5000 req/min total18 Global: &sentinel.Limit{Requests: 5000, Window: time.Minute},1920 // Extract user ID for per-user limiting21 UserIDExtractor: func(c *gin.Context) string {22 return c.GetHeader("X-User-ID")23 },24}
Configuration Reference
| Field | Type | Default | Description |
|---|---|---|---|
Enabled | bool | false | Enables the rate limiting middleware. |
Strategy | RateLimitStrategy | sentinel.SlidingWindow | Algorithm used for counting. Options: sentinel.SlidingWindow, sentinel.FixedWindow, sentinel.TokenBucket. |
ByIP | *Limit | nil | Per-IP rate limit. Each unique client IP gets its own counter. |
ByUser | *Limit | nil | Per-user rate limit. Requires a UserIDExtractor. |
ByRoute | map[string]Limit | nil | Per-route rate limits. Keys are exact route paths. |
Global | *Limit | nil | Global rate limit applied across all requests regardless of source. |
UserIDExtractor | func(*gin.Context) string | nil | Function to extract a user ID from the request for per-user limiting. |
Priority Order
When multiple dimensions are configured, Sentinel evaluates them in a specific order. The request is rejected as soon as any dimension's limit is exceeded — remaining dimensions are not checked.
| Priority | Dimension | Counter Key | Description |
|---|---|---|---|
| 1 (highest) | Per-Route | route:/path:IP | Checked first. Only applies if the request path matches a key in ByRoute. |
| 2 | Per-IP | ip:IP | Checked second. Applies to every request when ByIP is set. |
| 3 | Per-User | user:userID | Checked third. Only applies when ByUser is set and UserIDExtractor returns a non-empty string. |
| 4 (lowest) | Global | global | Checked last. A single counter shared across all requests. |
Independent Evaluation
/api/login is checked against the route limit and the IP limit and the user limit and the global limit (if all are configured). The request must pass every applicable check.Response Headers
Sentinel automatically sets standard rate limit headers on responses so clients can self-regulate. When a limit is exceeded, the client receives a 429 Too Many Requests response with a JSON body.
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | The maximum number of requests allowed in the current window. | 100 |
X-RateLimit-Remaining | The number of requests remaining in the current window. | 73 |
Retry-After | Seconds until the rate limit window resets. Only sent when the limit is exceeded (429 response). | 60 |
On a successful request, the response includes X-RateLimit-Limit and X-RateLimit-Remaining based on the per-IP limit. When a limit is exceeded, the response body is:
{"error": "Rate limit exceeded","code": "RATE_LIMITED"}
Per-Route Limits
Per-route limits let you apply different thresholds to different endpoints. This is especially useful for protecting sensitive routes like login, registration, and password reset endpoints with much stricter limits than the rest of your API.
1sentinel.Mount(r, nil, sentinel.Config{2 RateLimit: sentinel.RateLimitConfig{3 Enabled: true,45 // General API limit: 100 req/min per IP6 ByIP: &sentinel.Limit{Requests: 100, Window: time.Minute},78 // Strict limits on sensitive routes9 ByRoute: map[string]sentinel.Limit{10 // Login: 5 attempts per 15 minutes per IP11 "/api/login": {Requests: 5, Window: 15 * time.Minute},1213 // Registration: 3 per hour per IP14 "/api/register": {Requests: 3, Window: time.Hour},1516 // Password reset: 3 per hour per IP17 "/api/password-reset": {Requests: 3, Window: time.Hour},1819 // File upload: 10 per minute per IP20 "/api/upload": {Requests: 10, Window: time.Minute},21 },22 },23})
Route limits are keyed by the combination of the route path and the client IP. For example, a request to /api/login from IP 1.2.3.4 uses the counter key route:/api/login:1.2.3.4. This means each IP gets its own counter for each route-limited path.
Exact Path Matching
/api/login will not match /api/login/ (trailing slash) or /api/login?foo=bar (query parameters are stripped). Use the path as it appears in your Gin route definitions.How It Works
Sentinel uses sliding window counters stored entirely in memory. Each counter tracks a request count and a window expiration timestamp.
- When a request arrives, Sentinel looks up the counter for the relevant key (e.g.,
ip:1.2.3.4). - If no counter exists, or the current time is past the window expiration, a new counter is created with a count of 1 and a window end of
now + Window. - If the counter exists and the window has not expired, the count is incremented.
- If the count exceeds the configured limit, the request is rejected with a
429status.
A background goroutine runs every 30 seconds to clean up expired counters, preventing unbounded memory growth. The cleanup removes any counter whose window has passed.
// Internal counter structure (simplified)type rateLimitEntry struct {count int // Number of requests in the current windowwindowEnd time.Time // When the current window expires}// Cleanup runs every 30 seconds, removing expired entriesfunc (rl *RateLimiter) cleanup() {ticker := time.NewTicker(30 * time.Second)for range ticker.C {now := time.Now()for key, entry := range rl.counters {if now.After(entry.windowEnd) {delete(rl.counters, key)}}}}
Thread Safety
Dashboard Management
The Sentinel dashboard includes a dedicated Rate Limits page that gives you real-time visibility into your rate limit counters and configuration.
Live Counter States
The dashboard displays all active rate limit counters in real time. Each entry shows the counter key, the current request count, the window expiration time, and how many requests remain. Counters are automatically removed from the view when their window expires.
Edit Per-Route Limits
You can edit per-route limits directly from the dashboard without restarting your application. This is useful for responding to traffic spikes or adjusting thresholds after observing real-world patterns.
Reset Individual Counters
If a legitimate client gets rate-limited (e.g., during testing or after a deployment), you can reset their counter from the dashboard. This removes the specific counter key, allowing the client to send requests again immediately.
No Restart Required
Testing
You can verify rate limiting is working by sending rapid requests with curl and inspecting the response headers.
Check Rate Limit Headers
# Send a request and inspect rate limit headerscurl -v http://localhost:8080/api/hello 2>&1 | grep -i "x-ratelimit\|retry-after"# Expected output (first request):# < X-RateLimit-Limit: 100# < X-RateLimit-Remaining: 99
Hit the Rate Limit
# Send requests in a tight loop to trigger the limit# (adjust the count based on your configured limit)for i in $(seq 1 110); doSTATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/hello)echo "Request $i: HTTP $STATUS"done# You should see HTTP 200 for the first 100 requests,# then HTTP 429 once the limit is exceeded.
Test Per-Route Limits
# Test the login endpoint (5 requests per 15 minutes)for i in $(seq 1 7); doRESPONSE=$(curl -s -w "\nHTTP %{http_code}" \-X POST http://localhost:8080/api/login \-H "Content-Type: application/json" \-d '{"email":"test@example.com","password":"test"}')echo "Request $i: $RESPONSE"done# Requests 1-5: normal response# Requests 6-7: HTTP 429 with Retry-After header
Inspect a 429 Response
# After exceeding the limit, inspect the full 429 responsecurl -v http://localhost:8080/api/hello 2>&1# Response headers will include:# < HTTP/1.1 429 Too Many Requests# < X-RateLimit-Limit: 100# < X-RateLimit-Remaining: 0# < Retry-After: 60## Response body:# {"code":"RATE_LIMITED","error":"Rate limit exceeded"}
Limitations
The current rate limiter implementation has a few limitations to be aware of when planning your deployment.
| Limitation | Details | Workaround |
|---|---|---|
| In-memory only | All rate limit counters are stored in process memory. Counters are lost when the application restarts. | Accept that counters reset on restart. For most use cases, this is acceptable because windows are short (minutes/hours). |
| No cross-instance sharing | If you run multiple instances of your application behind a load balancer, each instance maintains its own independent counters. A client could effectively get N x limit requests across N instances. | Use sticky sessions at the load balancer level, or divide your limits by the number of instances (e.g., set 50 req/min per instance if you have 2 instances and want a 100 req/min effective limit). |
| Exact path matching | Per-route limits use exact string matching on the request path. Patterns, wildcards, and path parameters are not supported. | List each specific path you want to limit in the ByRoute map. |
Multi-Instance Deployments
Next Steps
- Full Configuration Reference — All rate limit fields and strategies
- Auth Shield — Brute-force protection for login endpoints
- WAF Configuration — Web Application Firewall rules and modes
- Dashboard — Explore the Rate Limits page and other dashboard features