Kulcare Docs
DeveloperAuthentication Documentation04 features

Rate Limiting

Overview

Rate limiting is implemented to protect our API from abuse and ensure fair usage across all clients. The system uses a sliding window algorithm with Redis/Rails cache for storage.

Implementation Architecture

Core Components

  1. RateLimitable Concern (parts/core/controllers/concerns/rate_limitable.rb)

    • Provides rate limiting functionality to controllers
    • Implements sliding window algorithm
    • Supports multiple limiting strategies (IP, User, API Key)
  2. ApiController Integration (parts/core/controllers/api_controller.rb)

    • All API controllers inherit rate limiting automatically
    • Default limit: rate_limit_threshold requests per rate_limit_period (falls back to 60 per minute), per signed-in user, or per IP when there is no user
    • Can be overridden per action with rate_limit
  3. Rails Cache Storage

    • Uses Rails.cache (Redis in production) for storing counters
    • Two keys per rate limit: count and window
    • Automatic expiration based on time period

Default Rate Limiting

Global Default

All API endpoints automatically get:

  • Limit: rate_limit_threshold per rate_limit_period minutes (falls back to 60 per minute)
  • Strategy: By signed-in user, or by IP when there is no user
  • Counter: One per route. The cache key is rate_limit:api_default:<controller>#<action>:<route pattern>:<user or IP>, so one action that serves several routes gives each route its own allowance. Identity::FeatureFlagsController#index answers both /identity/users/:user_id/feature_flags and /identity/tenants/:tenant_id/feature_flags, and the two no longer share a budget (#2510). The key uses the route pattern, not the ids in the path, so walking through ids does not earn a new allowance per id.

How It Works

class ApiController < ApplicationController
  include RateLimitable
  before_action :apply_default_rate_limit
end

Custom Rate Limiting

Per Controller/Action

Override defaults for specific endpoints:

class SessionController < ApiController
  # Login attempts: 5 per 5 minutes by IP
  rate_limit [:create], limit: 5, period: 5.minutes, by: :ip

  # Token refresh: 10 per minute by IP
  rate_limit [:refresh], limit: 10, period: 1.minute, by: :ip
end

Options for sign-in and OTP routes

Two options protect routes that send or check one-time codes (#2350):

  • prepend: true runs the limit before every other callback, including the sign-in check. Without it, a request with no valid token gets 401 before the limit counts it.
  • fail_closed: true blocks the request with 503 when the cache fails. The default allows the request. Use it on sign-in and OTP routes, where an unlimited request is worse than an unavailable one.
rate_limit %i[request_code confirm], limit: 30, period: 10.minutes, by: :ip, prepend: true, fail_closed: true
rate_limit :request_code, limit: 5, period: 10.minutes, by: :user, fail_closed: true

How rate_limit and the default combine

A rate_limit line replaces the default for the actions it names only. Every other action of the controller keeps the default (#2427). Declarations are inherited: a subclass keeps its parent's rate_limit actions and can add its own.

class RequestedPaymentsController < ApiController
  rate_limit :show,  limit: 60, period: 1.minute, by: :ip   # show: 60/min, no default
  rate_limit :order, limit: 30, period: 1.minute, by: :ip   # order: 30/min, no default
  def verify; end                                           # verify: the default
end

Before #2427 a single rate_limit line switched the default off for the whole controller, so actions like verify above had no limit at all. The fix newly applies the default to those actions. Declare an explicit, higher rate_limit on an action if the default is too tight for it (for example a route that many staff behind one clinic NAT call often).

domains/shared/test/controllers/every_api_action_rate_limited_test.rb walks every route to an ApiController subclass and fails if any action has neither its own rate_limit nor the default. A controller that skips apply_default_rate_limit for an action must give that action its own limit.

The routes with fail_closed: true: sessions create, registrations, identifier changes, reauth codes, invite verification, two-factor verify, confirm and disable, the verified order create routes, and the public appointment OTP routes.

The Identity password routes fail OPEN on purpose. Passwords::VerifyService counts each attempt per account and refuses when the cache cannot count it, so a limiter that answered 503 first would hide that guard (see domains/identity/controllers/passwords_controller.rb).

Rate Limiting Strategies

  1. By IP Address (default)

    rate_limit [:action], limit: 100, period: 1.hour, by: :ip
  2. By Authenticated User

    rate_limit [:action], limit: 50, period: 1.hour, by: :user
  3. By API Key

    rate_limit [:action], limit: 1000, period: 1.hour, by: :api_key

HTTP Response Headers

Rate limiting information is included in response headers:

X-RateLimit-Limit: 20
X-RateLimit-Remaining: 15
X-RateLimit-Reset: 1640995200

When rate limit is exceeded (HTTP 429):

Retry-After: 45

Testing Rate Limiting

Manual Testing with cURL

  1. Test Default Rate Limiting (the examples below assume rate_limit_threshold=20, as in the sample configuration; with the credential unset the default falls back to 60 per minute)

    # Make 21 requests quickly
    for i in {1..21}; do
      curl -i http://localhost:3000/api/v1/any_endpoint
      echo "Request $i"
    done
  2. Test Login Rate Limiting (5/5min)

    # Make 6 login attempts
    for i in {1..6}; do
      curl -X POST http://localhost:3000/auth/sign_in \
        -H "Content-Type: application/json" \
        -d '{"email":"test@example.com","password":"password"}'
    done
  3. Check Headers

    curl -i http://localhost:3000/api/v1/clinics
    # Look for X-RateLimit-* headers

Automated Testing

Run the rate limiting test suite:

bundle exec rspec spec/requests/rate_limiting_spec.rb

Load Testing

Use Apache Bench for load testing:

# Test with 100 requests, 10 concurrent
ab -n 100 -c 10 http://localhost:3000/api/v1/clinics/

Testing a Specific API

Example: Testing Clinics API Rate Limiting

  1. Find the endpoint

    # Check routes for clinics
    rails routes | grep clinics
  2. Test with multiple requests

    # Test clinics index endpoint
    for i in {1..25}; do
      curl -i -H "Authorization: Bearer YOUR_TOKEN" \
        http://localhost:3000/api/v1/clinics
      echo "Request $i - Check headers above"
      sleep 0.1
    done
  3. Verify rate limiting works After 20 requests, you should get:

    HTTP/1.1 429 Too Many Requests
    Retry-After: 45

Testing Authentication APIs

  1. Login endpoint (5 requests per 5 minutes)

    for i in {1..6}; do
      curl -X POST http://localhost:3000/auth/sign_in \
        -H "Content-Type: application/json" \
        -d '{"email":"test@example.com","password":"wrong"}'
      echo "Login attempt $i"
    done
  2. Expected behavior

    • First 5 requests: Normal responses (success/failure)
    • 6th request: 429 Too Many Requests

Application Logs

Rate limit violations are logged:

Rate limit exceeded for key: rate_limit:session_controller#create:192.168.1.100 in period: 300

Configuration

Environment Variables

Rate limiting can be configured via environment variables:

# Default rate limit configuration
rate_limit_threshold=20    # Number of requests allowed
rate_limit_period=1        # Time period in minutes

Examples:

# Allow 50 requests per 2 minutes
rate_limit_threshold=50
rate_limit_period=2

# Allow 100 requests per 5 minutes
rate_limit_threshold=100
rate_limit_period=5

On this page