marten-throttle
marten-throttle
Rate-limiting middleware for the Marten web framework. Built on top of Marten::Middleware and Marten.cache so it works with any cache backend the application is already using (memory, Redis, Memcached, ...).
Supports two strategies out of the box:
- fixed window — one cache increment per request, cheapest, accurate to within a window boundary.
- sliding window — two-bucket weighted counter, smooths out boundary bursts.
Installation
Add the dependency to shard.yml:
dependencies:
marten_throttle:
github: treagod/marten-throttle
Then shards install.
Configuration
In src/project.cr (or wherever you require your apps):
require "marten_throttle"
In config/settings/base.cr:
config.installed_apps = [
# ...
MartenThrottle::App,
]
config.middleware << MartenThrottle::Middleware
Marten.settings.throttle.default_limit = 100
Marten.settings.throttle.default_window = 1.minute
Marten.settings.throttle.default_strategy = :fixed_window
# Optional: key limits by your own user/session/IP identifier.
Marten.settings.throttle.client_identifier = ->(request : Marten::HTTP::Request) {
request.headers["X-Verified-Client-ID"]? || "global"
}
# Per-route policies. First matching rule wins.
Marten.settings.throttle.draw do
rule "/login", limit: 5, per: 1.minute, strategy: :sliding_window, methods: ["POST"]
rule "/api/*", limit: 30, per: 1.minute
rule %r{^/admin}, limit: 10, per: 1.minute
end
When a request exceeds its rule's limit the middleware short-circuits with a 429 Too Many Requests response carrying Retry-After and X-RateLimit-Limit headers.
Per-route rules
rule(matcher, limit, per, strategy = default_strategy, methods = nil):
| Argument | Type | Notes |
|---|---|---|
matcher |
String | Regex |
String supports trailing * (prefix match) or exact equality. |
limit |
Int32 |
Max allowed requests per window. |
per |
Time::Span |
Window length. Must be at least one second. |
strategy |
Symbol |
:fixed_window or :sliding_window. |
methods |
Array(String) | Nil |
If set, restricts the rule to those HTTP methods (case-insensitive). |
Rules are evaluated in declaration order. If no rule matches, the defaults apply. Limits must be greater than zero.
How keys are built
{cache_namespace}:{r<rule_index>|d}:{client_id}
r<rule_index> when a per-route rule matched, d for the fall-through default. One bucket per rule, not per concrete path — so /api/users/1 and /api/users/2 share the bucket of the /api/* rule. The strategy appends its own suffix.
client_id comes from client_identifier, trusted forwarded headers, or the "global" fallback bucket.
Identifying clients
By default, every request shares a single "global" bucket per rule. This is safe because it does not trust spoofable request headers, but it is a global limiter rather than a per-client limiter. For application-specific throttling, provide your own identifier:
Marten.settings.throttle.client_identifier = ->(request : Marten::HTTP::Request) {
request.headers["X-Verified-Client-ID"]? || "global"
}
Use a stable authenticated user ID, API key ID, tenant ID, or a proxy-verified IP address. Do not use a header that public clients can set directly. Empty identifiers are treated as "global".
Marten does not expose the peer connection address on the request, so IP-based identification has to come from headers. Reading those without a trusted proxy in front lets a client send arbitrary X-Forwarded-For values and shard itself into separate buckets, defeating the limit. Because of that, forwarded-header identification is off by default:
Marten.settings.throttle.trust_forwarded_headers = true
Enable this only when the app sits behind a proxy / load balancer that overwrites client-supplied forwarding headers before setting X-Forwarded-For or X-Real-IP. With the flag off and no custom identifier, every request uses the same "global" bucket per rule.
Cache backends
Marten.cache is used as-is. The Memory backend is fine for development. In production with multiple workers, use a shared backend (e.g. marten-redis-cache or marten-memcached-cache) so counters stay consistent across processes.
Limitations / not yet implemented
Planned follow-ups: explicit trusted-proxy configuration, IP allowlists, custom 429 templates, blocked-request logging, standard RateLimit-* headers, and a token-bucket strategy.
License
MIT
marten-throttle
- 1
- 0
- 0
- 0
- 3
- about 5 hours ago
- May 9, 2026
MIT License
Sat, 09 May 2026 19:08:32 GMT