Skip to content

Rate Limiting (Implementation)

This page covers the implementation of rate limiting: the middleware, Redis-backed counters, IP blocker, and suspicious-activity heuristics. For the public-facing limits and 429 responses, see API → Rate Limiting.

Source files:

Middleware chain

cmd/server/main.go wires three security middlewares onto the global Gin engine, in order:

r.Use(middleware.SecurityHeaders(cfg.Server.Env))
r.Use(app.IPBlocker.BlockMiddleware())
r.Use(app.IPBlocker.SuspiciousActivityMiddleware())

After those run for every request, the rate limiter attaches as per-route middleware on the auth endpoints (auth.POST("/login", app.RateLimiter.LoginLimit(), ...) and friends).

RateLimiter

RateLimiter.Limit(RateLimitConfig) is a generic per-IP limiter. The config:

type RateLimitConfig struct {
    KeyPrefix    string         // e.g. "login_ip"
    MaxRequests  int            // e.g. 10
    Window       time.Duration  // e.g. 15*time.Minute
    ErrorMessage string         // surfaced in the 429 body
}

For every request, the limiter builds {KeyPrefix}:{client_ip} and calls redisService.RateLimit(ctx, key, max, window), which runs the following Lua script atomically:

local current = redis.call("INCR", KEYS[1])
if tonumber(current) == 1 then
    redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current

If current > max, the middleware returns 429 Too Many Requests and aborts the request. If Redis returns an error, the limiter fails openc.Next() is called and the request proceeds. This is intentional: a Redis outage shouldn't take the API down.

Preset limits

RateLimiter exposes four named presets:

Method Limit Use
APILimit() 600 / 1m Baseline ceiling on every /api/v1/* route
RegisterLimit() 5 / 1h Registration, OTP send
LoginLimit() 10 / 15m Login (all variants), OTP verify, WebAuthn login
ResetPasswordLimit() 5 / 1h Reset-password submission

Targeted limiters stack on top of APILimit() — a registration request consumes from both the 600/min baseline and the 5/hour register bucket.

Add new presets in rate_limiter.go rather than passing raw RateLimitConfig from each handler — it keeps the limits centrally tunable.

IPBlocker

Two responsibilities:

  1. Block list enforcement — anyone on the list gets 403 Forbidden, full stop.
  2. Suspicious activity detection — pattern-match dangerous request shapes and auto-block.

Block list

blocked_ip:{ip} is a Redis string with a TTL. The value encodes the reason (blocked:exceeded rate limits 5 times). BlockMiddleware checks every request and returns:

{
  "error": "Your IP address has been temporarily blocked",
  "reason": "...",
  "message": "If you believe this is a mistake, please contact support"
}

Auto-block on violations

violations:{ip} is a counter incremented every time a rate limit is hit (when wired via AutoBlockOnRateLimit, which is available but not currently the default). Defaults from DefaultBlockConfig:

Setting Value
ViolationThreshold 5 violations
BlockDuration 24 hours
CheckWindow 1 hour (sliding)

When the threshold is crossed, the IP is added to blocked_ip and the violation counter resets.

Suspicious activity heuristic

SuspiciousActivityMiddleware runs three checks on every request:

  1. SQL-injection patterns in any query parameter value — ' OR '1'='1, ' OR 1=1, '; DROP TABLE, UNION SELECT, plus <script> and javascript: for good measure.
  2. Path-traversal patterns in the URL path — ../, ..\, %2e%2e%2f, %2e%2e/, ..%2f.
  3. Excessively long query strings — > 2000 characters of raw query.

A match triggers an immediate 48-hour block on the source IP and a 403 Forbidden response.

False positives

The heuristic is conservative — a user accidentally typing ' OR '1'='1 in a search bar will trip it. Operators can unblock via:

redis-cli DEL blocked_ip:<ip>
redis-cli DEL violations:<ip>

There is no admin UI for this today; it's a runbook action.

Adding a new rate-limited endpoint

  1. Add a preset method to RateLimiter if none of the existing presets fit.
  2. Attach it to the route in cmd/server/main.go:
auth.POST("/new-thing", app.RateLimiter.LoginLimit(), handler.NewThing)
  1. Document the limit on API → Rate Limiting.

Authenticated routes

Routes behind JWTAuth aren't rate-limited per-route today. They rely on:

  • The auth endpoints' rate limits keeping brute-force off the doorstep
  • The global IP blocker for repeated offenders
  • The user being identifiable, so abuse can be tied to an account and dealt with via admin action

If you need a per-user limit on an authenticated route, build a small wrapper that keys on user_id from the JWT context rather than on IP.