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/v2"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 Paths and Wildcard Patterns
/api/login does not match /api/login/ (query strings are ignored). Keys can also be patterns — /v1/* or /v1/** for a whole subtree, /api/apps/*/products for one segment, or /api/apps/*/products/** for both. Every path matching a pattern shares that pattern's counter, so rotating sub-paths can't reset a client's budget. When an exact key and a pattern both match, the exact key wins; otherwise the longest pattern does.How It Works
Counters live in process memory, one per key (e.g. ip:1.2.3.4 or route:/api/login:1.2.3.4). How a counter decides is set by RateLimitConfig.Strategy:
| Strategy | How it counts |
|---|---|
sentinel.SlidingWindow (default) | Keeps the current and previous window's counts. A request is allowed while previous × overlap + current is under the limit, where overlap is how much of the previous window still falls inside the window ending now. At a boundary the whole previous window still counts, so a burst straddling it is limited. Rejected requests are not counted. |
sentinel.FixedWindow | A window starts at a client's first request and resets completely when it ends. Cheap, but up to twice the limit can pass across a boundary. Rejected requests count. |
sentinel.TokenBucket | A bucket holding up to Requests tokens, refilled at Requests per Window. Each request spends one; a burst up to the limit is allowed, then a steady rate. |
When a limit is exceeded the request is rejected with 429. A background goroutine runs every 30 seconds and removes counters that no longer affect any decision, preventing unbounded memory growth. An unknown strategy falls back to sliding window and is reported by ValidateConfig.
Before v2.3.0
Strategy was never read: every limit was a fixed window whatever the config said. If you relied on that behavior, set Strategy: sentinel.FixedWindow explicitly.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
/, or an unmatchable pattern is rejected with 400 and nothing changes — and every change is written to the audit log. Since v2.6.0 a route-limit edit is also stored, so it survives a restart and reaches every replica within Storage.SyncInterval; the response carries a warning when the storage backend could not keep it. (Before v2.3.0, route-limit edits updated only the dashboard's copy of the config and were never enforced.)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"}
Across Replicas
By default, counters live in process memory. If you run several instances behind a load balancer, each one counts on its own, so a client gets N × limit requests across N instances. To share the counters, set Config.Counters to a shared store. Every replica then counts against the same numbers:
1import (2 "github.com/MUKE-coder/sentinel/v2/redisstore"3 "github.com/redis/go-redis/v9"4)56client := redis.NewClient(&redis.Options{7 Addr: "redis:6379",8 ReadTimeout: 250 * time.Millisecond, // bound how long a request waits on Redis9})1011sentinel.Mount(r, nil, sentinel.Config{12 Counters: redisstore.New(client),13 RateLimit: sentinel.RateLimitConfig{14 Enabled: true,15 ByIP: &sentinel.Limit{Requests: 100, Window: time.Minute},16 },17})
- Each decision runs as one Lua script, so two replicas can't both take the last slot. All three strategies behave the same as they do in memory.
- If Redis is unreachable, requests are allowed rather than failed. The error is logged at most once a minute.
- If several applications share one Redis, give each one its own
redisstore.WithPrefix. - Per-IP limits key on the client address. Behind a load balancer, set
WAF.TrustedProxies, or every request counts against the balancer's address.
examples/multi-replica in the repository runs two replicas behind Caddy, with a shared Redis and Postgres.
Limitations
The rate limiter has a few limitations to keep in mind when planning your deployment.
| Limitation | Details | Workaround |
|---|---|---|
| In-memory by default | Without Config.Counters, counters live in process memory. They reset when the application restarts, and each instance behind a load balancer counts on its own. | Set Config.Counters: redisstore.New(client). See Across Replicas. |
| Dashboard edits win over your config | Since v2.6.0 a route limit changed from the dashboard is stored and keeps applying after a restart — including after a deploy that changes ByRoute in code. | DELETE /sentinel/api/settings/live discards the stored settings and puts your configured values back on every replica. |
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