Rate Limiting

security

Rate Limiting

Definition

Rate limiting is a technique that controls the number of requests a client can make to a service within a defined time window. It serves as a critical defense mechanism against abuse, denial-of-service attacks, resource exhaustion, and unfair resource monopolization by individual clients. By enforcing request quotas, rate limiting ensures that no single client can overwhelm the system, preserving service availability and fairness for all users.

Rate limiting operates on a fundamentally different principle than load balancing and backpressure, though all three manage traffic flow. Load balancing distributes traffic across multiple servers to maximize throughput. Backpressure propagates flow control signals upstream to slow producers when consumers are overwhelmed. Rate limiting, by contrast, enforces hard ceilings on request frequency regardless of available capacity -- even if the system could handle more requests, the limit is enforced to prevent abuse and ensure fairness.

In API-centric architectures, rate limiting is typically enforced at the API Gateway level, where it can be applied consistently across all endpoints before requests reach backend services. This centralized enforcement prevents rate limit bypass through direct backend access and ensures that all clients, whether human users, automated scripts, or third-party integrations, are subject to the same policies.

Rate Limiting Algorithms

Several algorithms exist for implementing rate limiting, each with different characteristics regarding burst handling, memory efficiency, and fairness:

Token Bucket

The token bucket algorithm maintains a bucket with a fixed capacity that refills at a constant rate. Each request consumes one token. If the bucket is empty, the request is rejected. This algorithm naturally allows short bursts up to the bucket capacity while enforcing a sustained rate.

PropertyValue
Burst handlingAllows bursts up to bucket capacity
Sustained rateEnforced by refill rate
Memory per clientO(1) -- counter + timestamp
FairnessGood (refill is constant regardless of usage)
Prismatic usagePrimary algorithm for API gateway
# Token bucket implementation
defmodule PrismaticWeb.RateLimiter.TokenBucket do
  @moduledoc "Token bucket rate limiter with ETS-backed state"

  @type bucket :: %{
    tokens: float(),
    last_refill: integer(),
    capacity: pos_integer(),
    refill_rate: float()
  }

  @doc "Check if a request is allowed and consume a token"
  @spec check_rate(String.t(), pos_integer(), float()) ::
    {:allow, remaining :: non_neg_integer()} | {:deny, retry_after :: pos_integer()}
  def check_rate(client_id, capacity, refill_rate_per_second) do
    now = System.monotonic_time(:millisecond)

    case :ets.lookup(:rate_limiter_buckets, client_id) do
      [{^client_id, tokens, last_refill}] ->
        elapsed = (now - last_refill) / 1_000
        new_tokens = min(capacity, tokens + elapsed * refill_rate_per_second)

        if new_tokens >= 1.0 do
          :ets.insert(:rate_limiter_buckets, {client_id, new_tokens - 1.0, now})
          {:allow, trunc(new_tokens - 1.0)}
        else
          retry_after = trunc((1.0 - new_tokens) / refill_rate_per_second * 1_000)
          {:deny, retry_after}
        end

      [] ->
        :ets.insert(:rate_limiter_buckets, {client_id, capacity - 1.0, now})
        {:allow, capacity - 1}
    end
  end
end

Sliding Window Log

The sliding window log algorithm maintains a timestamped log of recent requests. When a new request arrives, expired entries are removed, and the request is allowed if the log size is below the limit. This provides precise rate limiting but requires more memory.

PropertyValue
Burst handlingNo burst allowance (strict window)
PrecisionExact (no boundary effects)
Memory per clientO(n) where n = limit (stores timestamps)
FairnessExcellent (precise per-window enforcement)
Prismatic usageUsed for high-security endpoints

Sliding Window Counter

A hybrid approach combining fixed window counters with interpolation to approximate a sliding window. It uses two fixed window counters (current and previous) and interpolates based on the current position within the window.

PropertyValue
Burst handlingLimited (smoothed by interpolation)
PrecisionApproximate (good enough for most use cases)
Memory per clientO(1) -- two counters + timestamps
FairnessGood (eliminates fixed window boundary spikes)
Prismatic usageUsed for general API endpoints

Algorithm Comparison

AlgorithmMemoryPrecisionBurstComplexityBest For
Token BucketO(1)GoodAllows controlled burstsLowGeneral purpose, API gateway
Sliding Window LogO(n)ExactNo burstsMediumHigh-security, audit-sensitive
Sliding Window CounterO(1)ApproximateLimitedLowHigh-throughput APIs
Fixed WindowO(1)Low (boundary issues)Allows 2x burst at boundaryLowestSimple use cases only
Leaky BucketO(1)GoodSmooths all burstsLowQueue-based systems

Multi-Level Enforcement

The Prismatic Platform implements rate limiting at multiple levels, each protecting a different boundary:

LevelScopeLimit ExamplePurpose
API GatewayPer-client, per-endpoint100 req/min, 5000 req/hrProtect backend services
Per-UserPer authenticated user1000 req/hr across all endpointsFair usage enforcement
Per-EndpointPer specific API routePOST /scans: 10 req/hrProtect expensive operations
Per-RolePer RBAC roleAdmin: 10000/hr, Viewer: 1000/hrTiered access
External APIPer third-party providerShodan: calibrated to tierRespect provider quotas
OSINT PipelinePer data source in BroadwayConfigured per providerPrevent API ban
GlobalSystem-wide safety limit50000 req/min totalSystem protection
# Multi-level rate limiting plug
defmodule PrismaticWeb.Plugs.RateLimiter do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, opts) do
    client_id = extract_client_id(conn, opts[:by])

    limits = opts[:limits] || default_limits()

    case check_all_limits(client_id, limits) do
      {:allow, remaining} ->
        conn
        |> put_resp_header("x-ratelimit-remaining", to_string(remaining))
        |> put_resp_header("x-ratelimit-limit", to_string(hd(limits) |> elem(2)))

      {:deny, retry_after} ->
        conn
        |> put_resp_header("retry-after", to_string(div(retry_after, 1000)))
        |> put_resp_header("x-ratelimit-remaining", "0")
        |> put_status(429)
        |> Phoenix.Controller.json(%{
          error: "Rate limit exceeded",
          retry_after_ms: retry_after
        })
        |> halt()
    end
  end

  defp extract_client_id(conn, :token_sub) do
    case conn.assigns[:current_user] do
      %{id: id} -> "user:#{id}"
      _ -> conn.remote_ip |> :inet.ntoa() |> to_string()
    end
  end

  defp extract_client_id(conn, :ip) do
    conn.remote_ip |> :inet.ntoa() |> to_string()
  end

  defp default_limits do
    [{60, :second, 100}, {3600, :second, 5000}]
  end
end

Response Headers

Rate-limited APIs communicate quota status through standardized HTTP response headers:

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed in the window100
X-RateLimit-RemainingRequests remaining in the current window42
X-RateLimit-ResetUnix timestamp when the window resets1706745600
Retry-AfterSeconds to wait before retrying (on 429)30

The HTTP 429 (Too Many Requests) status code indicates that rate limiting has been applied. Well-behaved clients should check the Retry-After header and implement exponential backoff rather than immediately retrying.

External API Rate Limiting

The Prismatic Platform manages rate limits for external OSINT data sources used in the EASM pipeline. Each provider has different quotas, and exceeding them can result in temporary or permanent API key revocation:

ProviderRate LimitAlgorithmPrismatic Strategy
Shodan1 req/sec (free), varies by tierToken bucketCalibrated to subscription tier
CensysVaries by tier (0.4-10 req/sec)Sliding windowConservative with retry backoff
GreyNoise100 req/day (community), variesToken bucketBulk lookups to minimize requests
Certificate TransparencyVaries by log operatorPer-log limitingRound-robin across CT logs
DNS resolversProvider-dependentLeaky bucketDistributed across resolvers
# External API rate limiter with provider-specific configuration
defmodule PrismaticPerimeter.RateLimiter.External do
  @moduledoc "Rate limiter for external OSINT API integrations"

  @provider_limits %{
    shodan: {1, :second},        # 1 request per second
    censys: {2, :second},        # 2 requests per second
    greynoise: {100, :day},      # 100 requests per day (community)
    virustotal: {4, :minute}     # 4 requests per minute (free)
  }

  @spec with_rate_limit(atom(), (-> result)) :: result when result: term()
  def with_rate_limit(provider, fun) do
    {limit, window} = Map.fetch!(@provider_limits, provider)

    case TokenBucket.check_rate("external:#{provider}", limit, rate_per_second(limit, window)) do
      {:allow, _remaining} ->
        fun.()

      {:deny, retry_after} ->
        Process.sleep(retry_after)
        with_rate_limit(provider, fun)
    end
  end
end

Context in Prismatic

The Prismatic Platform implements rate limiting at multiple levels through the Plug middleware system. The API Gateway enforces per-client request limits through PrismaticWeb.Plugs.RateLimiter, protecting backend services from excessive load. External OSINT API integrations (Shodan, Censys, GreyNoise) use provider-specific rate limiters to respect quotas. Broadway pipelines use backpressure-aware rate limiting to control throughput in data processing operations.

The Hammer library provides the underlying rate limiting implementation with ETS-backed state for single-node deployments and pluggable backends (Redis, Mnesia) for distributed configurations. This aligns with the platform's pattern of using ETS for development and distributed backends for production.

Integration with Other Patterns

Rate limiting works in concert with several other traffic management patterns:

PatternRelationshipCombined Behavior
Circuit BreakerComplementaryRate limit prevents overload; circuit breaker handles failures
BackpressureLayeredRate limit at edges; backpressure within pipelines
Load BalancingComplementaryLoad balancer distributes; rate limiter caps per-client
Retry with BackoffClient-sideRate limit enforces server policy; client respects it
Bulkhead IsolationComplementaryRate limit per-client; bulkhead per-resource pool
  • API Gateway - Primary enforcement point for API rate limits
  • REST API - HTTP interface protected by rate limiting
  • Load Balancing - Complementary traffic distribution technique
  • Backpressure - Internal flow control coordinating with rate limits
  • Circuit Breaker - Failure-triggered traffic halt complementing rate limits
  • Plug - Elixir middleware implementing rate limit enforcement
  • Shodan - External API with provider-specific rate limits
  • Censys - External API with tier-based rate limits
  • RBAC - Per-role rate limit tiers
  • Observability - Monitoring rate limit metrics and rejections

See Also

  • Architecture - Traffic management architecture
  • Apps - Prismatic API gateway application
  • Technologies - Hammer library and rate limiting infrastructure

Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL