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

Repository

marten-throttle

Owner
Statistic
  • 1
  • 0
  • 0
  • 0
  • 3
  • about 5 hours ago
  • May 9, 2026
License

MIT License

Links
Synced at

Sat, 09 May 2026 19:08:32 GMT

Languages