Token-bucket rate limiting with per-IP tracking, path-based rules, automatic cleanup, and bandwidth limiting.
ALOS provides a high-performance rate limiting system based on the token bucket
algorithm. Rate limits are enforced per client IP with support for path-based
rules, automatic blocking, custom limit responses, and dynamic rule management. The system
supports two levels of custom responses: per-rule callbacks via
OnLimit on individual rules, and a global fallback via
s.OnRateLimit().
Each client gets a bucket that refills at a configurable rate, allowing burst traffic up to the bucket size.
Limits tracked per source IP with sharded concurrent maps. Integrates with Real IP middleware for proxy-aware resolution.
Expired buckets are automatically pruned. Blocked IPs auto-unblock after the configured duration.
Per-rule OnLimit callbacks for route-specific responses plus a
global s.OnRateLimit() fallback for all rules.
Three pattern types: exact paths, prefix with /* wildcard, and regex
with ~ prefix. Most specific match wins.
Separate bandwidth limiting with per-connection and global throttling for upload and download.
Enable rate limiting by adding rules to your server. Each rule specifies a path pattern, a request limit, a time window, and how long to block offending IPs:
s := core.New(core.Config{Addr: ":443"}) s.SetRateLimitRules([]core.RateLimitRule{ { Path: "/api/*", MaxReqs: 100, Window: 60 * time.Second, BlockFor: 5 * time.Minute, }, })
When a client at a given IP exceeds 100 requests to any path under
/api/ within a 60-second window, they are blocked for
5 minutes. During the block, all requests from that IP matching the rule
are rejected with a 429 status.
s.SetRateLimitRules([]core.RateLimitRule{ { Path: "/api/*", MaxReqs: 100, Window: 60 * time.Second, BlockFor: 5 * time.Minute, }, { Path: "/health", MaxReqs: 1000, Window: 60 * time.Second, BlockFor: 30 * time.Second, }, { Path: "/*", MaxReqs: 300, Window: 60 * time.Second, BlockFor: 2 * time.Minute, }, })
Rules are matched by specificity — the most specific path wins. An exact match like
/health takes priority over a prefix match like /*. This means
you can set a generous limit for health checks while keeping a tighter global limit.
The RateLimitRule struct defines a rate limit:
| Field | Type | Description |
|---|---|---|
Path |
string |
Path pattern to match — exact /login, prefix
/api/*, or regex ~^/users/\d+$
|
MaxReqs |
int64 |
Maximum number of requests allowed within the time window |
Window |
time.Duration |
Time window for counting requests (e.g. 60 * time.Second) |
BlockFor |
time.Duration |
How long to block the IP after exceeding the limit (e.g.
5 * time.Minute)
|
OnLimit |
RateLimitFunc |
Per-rule callback when this specific rule triggers. Signature:
func(event RateLimitEvent, req *Request, resp *Response) bool.
Return true if handled.
|
type RateLimitRule struct { Path string MaxReqs int64 Window time.Duration BlockFor time.Duration OnLimit RateLimitFunc } type RateLimitFunc func(event RateLimitEvent, req *Request, resp *Response) bool
Three matching modes are supported. The engine picks the most specific match for each request:
| Type | Syntax | Example | Matches |
|---|---|---|---|
| Exact | Full path, no wildcard | /login |
Only /login (highest priority) |
| Exact | Full path | /health |
Only /health |
| Prefix | Path with /* |
/api/* |
/api/users, /api/posts/1, etc. |
| Prefix | Catch-all | /* |
All paths (lowest priority prefix) |
| Regex | Starts with ~ |
~^/users/\d+$ |
/users/123, /users/456 |
| Regex | Starts with ~ |
~^/api/v[0-9]+/admin |
/api/v1/admin, /api/v2/admin/users |
s.SetRateLimitRules([]core.RateLimitRule{ {Path: "/login", MaxReqs: 5, Window: 60 * time.Second, BlockFor: 10 * time.Minute}, {Path: "/api/*", MaxReqs: 100, Window: 60 * time.Second, BlockFor: 5 * time.Minute}, {Path: "~^/users/\\d+$", MaxReqs: 50, Window: 60 * time.Second, BlockFor: 1 * time.Minute}, {Path: "/health", MaxReqs: 1000, Window: 60 * time.Second, BlockFor: 30 * time.Second}, {Path: "/*", MaxReqs: 300, Window: 60 * time.Second, BlockFor: 2 * time.Minute}, })
Specificity order: Exact match > Prefix match (longer path first)
> Regex match. A request to /health will always use the exact
/health rule, never the /* catch-all.
OnLimit)Each rule can have its own OnLimit callback that fires when that specific rule
is triggered. The callback receives a RateLimitEvent with full context about
the violation. Return true to indicate the response was handled.
s.SetRateLimitRules([]core.RateLimitRule{ { Path: "/api/*", MaxReqs: 100, Window: 60 * time.Second, BlockFor: 5 * time.Minute, OnLimit: func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { var buf [64]byte b := buf[:0] b = append(b, `{"error":"api rate limited","retry_after":`...) b = strconv.AppendInt(b, int64(event.RetryAfter.Seconds())+1, 10) b = append(b, '}') resp.Status(429).JSON(b) return true }, }, })
{
Path: "/login",
MaxReqs: 5,
Window: 60 * time.Second,
BlockFor: 10 * time.Minute,
OnLimit: func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool {
var buf [256]byte
b := buf[:0]
b = append(b, `<h1>Too Many Login Attempts</h1><p>Try again in `...)
b = strconv.AppendInt(b, int64(event.RetryAfter.Seconds())+1, 10)
b = append(b, ` seconds.</p>`...)
resp.Status(429).HTML(string(b))
return true
},
},
{
Path: "~^/users/\\d+$",
MaxReqs: 50,
Window: 60 * time.Second,
BlockFor: 1 * time.Minute,
OnLimit: func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool {
log.Printf("rate limit hit: ip=%s path=%s", event.IP, event.Path)
return false
},
},
Returning false from OnLimit means the response was
not handled — ALOS will fall through to the global handler, or if
none is set, send a default 429 response.
s.OnRateLimit)The global handler is a catch-all that fires for any rate limit violation
that hasn't been handled by a per-rule OnLimit. This is where you put logging,
metrics, and default error responses. Set it once on the server:
s.OnRateLimit(func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { log.Printf("[RATE-LIMIT] ip=%s path=%s rule=%s retry_after=%s", event.IP, event.Path, event.Rule.Path, event.RetryAfter) var buf [256]byte b := buf[:0] b = append(b, `<html><body><h1>Slow Down</h1><p>Too many requests. Try again in `...) b = strconv.AppendInt(b, int64(event.RetryAfter.Seconds())+1, 10) b = append(b, ` seconds.</p></body></html>`...) resp.Status(429).HTML(string(b)) return true })
When a rate limit is triggered, ALOS follows this order:
OnLimit — If the matched rule has an
OnLimit callback, it runs first. If it returns true, the
request is fully handled.
s.OnRateLimit — If the per-rule handler
returned false or there is no per-rule handler, the global handler runs. If
it returns true, the request is handled.false, ALOS sends a plain 429 Too Many Requests response.
s.OnRateLimit(func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { j := core.AcquireJSON() resp.Status(429).JSON(j.Marshal(map[string]any{ "error": "rate_limit_exceeded", "rule": event.Rule.Path, "retry_after_seconds": int64(event.RetryAfter.Seconds()) + 1, })) j.Release() return true })
API routes get a JSON error, everything else falls through to the global HTML handler:
s.SetRateLimitRules([]core.RateLimitRule{ { Path: "/api/*", MaxReqs: 100, Window: 60 * time.Second, BlockFor: 5 * time.Minute, OnLimit: func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { var buf [64]byte b := buf[:0] b = append(b, `{"error":"api rate limited","retry_after":`...) b = strconv.AppendInt(b, int64(event.RetryAfter.Seconds())+1, 10) b = append(b, '}') resp.Status(429).JSON(b) return true }, }, {Path: "/health", MaxReqs: 1000, Window: 60 * time.Second, BlockFor: 30 * time.Second}, {Path: "/*", MaxReqs: 300, Window: 60 * time.Second, BlockFor: 2 * time.Minute}, }) s.OnRateLimit(func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { log.Printf("[RATE-LIMIT] ip=%s path=%s rule=%s retry_after=%s", event.IP, event.Path, event.Rule.Path, event.RetryAfter) var buf [256]byte b := buf[:0] b = append(b, `<html><body><h1>Slow Down</h1><p>Too many requests. Try again in `...) b = strconv.AppendInt(b, int64(event.RetryAfter.Seconds())+1, 10) b = append(b, ` seconds.</p></body></html>`...) resp.Status(429).HTML(string(b)) return true })
Both per-rule and global handlers receive a RateLimitEvent with full context
about the violation:
| Field | Type | Description |
|---|---|---|
IP |
string |
Client IP address that triggered the limit |
Path |
string |
The actual request path (e.g. /api/users/123) |
Rule |
RateLimitRule |
The rule that was matched — access event.Rule.Path,
event.Rule.MaxReqs, etc.
|
RetryAfter |
time.Duration |
How long until the block expires — use
event.RetryAfter.Seconds() for the numeric value
|
type RateLimitEvent struct { IP string Path string Rule RateLimitRule RetryAfter time.Duration }
OnLimit: func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { log.Printf("ip=%s hit rule=%s (max=%d/%s) path=%s retry=%s", event.IP, event.Rule.Path, event.Rule.MaxReqs, event.Rule.Window, event.Path, event.RetryAfter, ) resp.Status(429).SetHeader("Retry-After", strconv.Itoa(int(event.RetryAfter.Seconds())+1)) resp.JSON([]byte(`{"error":"rate limited"}`)) return true },
Manage rules dynamically at runtime. Rules can be added, removed, or replaced while the server is running without any downtime:
| Method | Description |
|---|---|
s.SetRateLimitRules([]RateLimitRule) |
Replace all rules at once (atomic swap) |
s.AddRateLimitRule(RateLimitRule) |
Add a single rule without affecting existing rules |
s.RemoveRateLimitRule(path string) |
Remove a rule by its path pattern |
s.OnRateLimit(fn RateLimitFunc) |
Set the global rate limit handler (called when no per-rule handler handles the event) |
s.AddRateLimitRule(core.RateLimitRule{ Path: "/webhook", MaxReqs: 20, Window: 60 * time.Second, BlockFor: 5 * time.Minute, })
s.RemoveRateLimitRule("/webhook")
s.SetRateLimitRules([]core.RateLimitRule{ {Path: "/api/*", MaxReqs: 200, Window: 60 * time.Second, BlockFor: 5 * time.Minute}, {Path: "/*", MaxReqs: 500, Window: 60 * time.Second, BlockFor: 1 * time.Minute}, })
You can build an admin API to manage rate limits at runtime:
admin := s.Router.Group("/admin", authMiddleware) admin.POST("/rate-limit", func(req *core.Request, resp *core.Response) { s.AddRateLimitRule(core.RateLimitRule{ Path: "/api/heavy/*", MaxReqs: 10, Window: 60 * time.Second, BlockFor: 10 * time.Minute, }) resp.Status(200).JSONString(`{"status":"rule added"}`) }) admin.DELETE("/rate-limit/:path", func(req *core.Request, resp *core.Response) { s.RemoveRateLimitRule(req.ParamValue("path")) resp.Status(200).JSONString(`{"status":"rule removed"}`) })
Separate from request rate limiting, bandwidth limiting controls the actual data transfer
speed. ALOS uses BandwidthConfig for per-connection and global throttling:
| Field | Type | Description |
|---|---|---|
MaxUploadRate |
int64 |
Max upload speed in Mbps (0 = unlimited) |
MaxDownloadRate |
int64 |
Max download speed in Mbps (0 = unlimited) |
BurstSize |
int64 |
Burst allowance in Mbps (token bucket max tokens) |
s := core.New(core.Config{ Addr: ":443", ConnBandwidth: core.BandwidthConfig{ MaxUploadRate: 8, MaxDownloadRate: 40, BurstSize: 16, }, GlobalBandwidth: core.BandwidthConfig{ MaxUploadRate: 400, MaxDownloadRate: 1600, BurstSize: 800, }, })
This creates a ConnectionLimiter per connection (8 Mbps up, 40 Mbps down with 16
Mbps burst) and a GlobalLimiter for server-wide throttling (400 Mbps up, 1600
Mbps
down with 800 Mbps burst). Both use token-bucket algorithms internally.
Connection vs Global: Per-connection limits prevent any single client from hogging bandwidth, while the global limit ensures total server throughput stays within your infrastructure capacity.
For file downloads via SendFile, you can set a per-download speed limit in
megabits per second (1–250,000 Mbps):
Runtime note: SendFile currently uses the streamed-response
compatibility path. On the native Linux io_uring worker runtime, this API
is still unavailable.
s.Router.GET("/download/:file", func(req *core.Request, resp *core.Response) { resp.SendFile( "./files/" + req.ParamValue("file"), core.WithAttachment("download.zip"), core.WithRateLimit(50), ) })
| Option | Description |
|---|---|
WithRateLimit(mbps int64) |
Throttle download speed. Range: 1–250,000 Mbps. Internally converts to
bytes/sec (mbps * 125000). |
WithRateLimitBurst(mbps, burstMbps int64) |
Same as WithRateLimit but with a custom burst size in Mbps. Allows
initial burst before throttling kicks in. |
resp.SendFile( "./files/large-archive.tar.gz", core.WithAttachment("large-archive.tar.gz"), core.WithRateLimitBurst(100, 400), )
This allows an initial burst at 400 Mbps, then throttles to 100 Mbps for the remainder.
A complete production rate limiting setup with multiple rules, per-rule API handler, and a global HTML fallback:
s.SetRateLimitRules([]core.RateLimitRule{ { Path: "/api/*", MaxReqs: 100, Window: 60 * time.Second, BlockFor: 5 * time.Minute, OnLimit: func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { var buf [64]byte b := buf[:0] b = append(b, `{"error":"api rate limited","retry_after":`...) b = strconv.AppendInt(b, int64(event.RetryAfter.Seconds())+1, 10) b = append(b, '}') resp.Status(429).JSON(b) return true }, }, { Path: "/health", MaxReqs: 1000, Window: 60 * time.Second, BlockFor: 30 * time.Second, }, { Path: "~^/users/\\d+$", MaxReqs: 50, Window: 60 * time.Second, BlockFor: 1 * time.Minute, }, { Path: "/*", MaxReqs: 300, Window: 60 * time.Second, BlockFor: 2 * time.Minute, }, }) s.OnRateLimit(func(event core.RateLimitEvent, req *core.Request, resp *core.Response) bool { log.Printf("[RATE-LIMIT] ip=%s path=%s rule=%s retry_after=%s", event.IP, event.Path, event.Rule.Path, event.RetryAfter) var buf [256]byte b := buf[:0] b = append(b, `<html><body><h1>Slow Down</h1><p>Too many requests. Try again in `...) b = strconv.AppendInt(b, int64(event.RetryAfter.Seconds())+1, 10) b = append(b, ` seconds.</p></body></html>`...) resp.Status(429).HTML(string(b)) return true })