Skip to main content
Guides

Rate limiting

How RestWard rate limits work, choosing values, plan ceilings, and client-side backoff patterns for 429 responses.

RestWard enforces rate limits at multiple levels to protect your APIs and ensure fair usage. Every request that exceeds a limit receives an HTTP 429 Too Many Requests response with standard headers that help clients back off gracefully.

How rate limiting works

RestWard applies four independent limits in order. A request is rejected as soon as it exceeds the first limit it encounters; it never consumes quota from limits checked later in the chain.

Limit Scope Key Window When applied
Platform global Entire deployment ratelimit:platform:global Configurable (default 60s) Every request to every tenant/API
Platform tenant One organization ratelimit:platform:tenant:{orgId} Configurable (default 60s) Every request to any API of that org
Caller quota One authenticated caller Per API key, JWT subject, or mTLS cert 1 minute All routes of the API
Endpoint limit One route + caller Per endpoint path + caller 1 minute That specific endpoint

Platform limits (#188) are fleet-wide: the global bucket is shared by every tenant and API in the deployment, and the tenant bucket is shared by every API of one organization. They are checked first so a refused request never consumes caller or endpoint quota.

Caller quota applies to every authenticated caller. API keys have their own RateLimitPerMinute; JWT and mTLS callers use the method's configured quota (or a default of 60 requests per minute). Anonymous requests have no caller quota.

Endpoint limit is the per-endpoint rate limit you can configure in the dashboard for each endpoint (#149). It is counted per caller on that route.

Headers on every response

Every successful response and every 429 rejection carries the three standard rate-limit headers for the quota closest to running out (the one with the fewest remaining requests):

  • X-RateLimit-Limit — the quota ceiling for that window
  • X-RateLimit-Remaining — requests left in the current window
  • X-RateLimit-Reset — seconds until the window resets

On a 429 response only, you also receive:

  • Retry-After — seconds to wait before retrying (same value as X-RateLimit-Reset)
  • X-RateLimit-Scope — which limit tripped: global, tenant, caller, or endpoint

Example 429 response

HTTP
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 42
X-RateLimit-Scope: caller

{"error": "Rate limit exceeded. Try again later."}

Backing store

When a distributed store is configured (Redis), counters are fleet-wide across all API runtime replicas. If the store is unavailable or not configured, the middleware fails open to in-process counters so requests are never rejected due to store unavailability; the quota then applies per replica until the store recovers.

Redis implementation (RedisDistributedRateLimitStore): a Lua script atomically increments the counter and sets the TTL only on the first request in the window, ensuring consistent counts across concurrent callers and replicas.

Choosing rate limit values

Per-key limits

Each API key has its own RateLimitPerMinute value set when the key is created. You can edit it later in the dashboard or via the API. A new key defaults to 60 requests per minute.

JWT and mTLS method limits

JWT and mTLS authentication methods can also have a configured rate limit. When not set or below 1, they use the same default: 60 requests per minute (DefaultPrincipalRateLimitPerMinute).

Endpoint limits

Each endpoint can have its own RateLimitPerMinute configured in the dashboard. This limit is counted per caller on that specific route, so different consumers hit their own buckets.

Platform limits

Platform administrators can configure two fleet-wide limits in Platform Settings:

  • Global rate limit per minute — applies to every request across the entire deployment
  • Per-tenant rate limit per minute — applies to every request for one organization across all its APIs

Both use the rate limit window seconds setting (default: 60 seconds) and include a burst allowance that is added to the scaled per-window allowance.

Calculation: Allowance = (RatePerMinute * WindowSeconds / 60) + BurstAllowance

Plan ceilings and entitlements

Your organization's billing plan may impose ceilings on the maximum rate limit you can configure. The MaxKeyRateLimitPerMinute value in the billing model is the highest value allowed; any higher value you set for API keys or JWT/mTLS authentication methods is automatically lowered to this ceiling.

Example: On a plan with MaxKeyRateLimitPerMinute: 1000, if you attempt to set a rate limit of 1500 on an API key or a JWT/mTLS method, it will be clamped to 1000 requests per minute.

For current plan limits and how they interact with rate limiting, see the pricing page or your organization's billing settings.

Handling 429 responses (client-side backoff)

When you receive a 429 response, honor the Retry-After header. Do not retry immediately; wait at least that many seconds before sending the next request.

curl

Shell
# First request (may hit the limit)
curl -H "X-API-Key: rw_abcd1234..." \
  https://YOUR-API-HOSTNAME/api/public/users

# If you receive 429 with Retry-After: 42, wait before retrying
sleep 42

# Retry
curl -H "X-API-Key: rw_abcd1234..." \
  https://YOUR-API-HOSTNAME/api/public/users

JavaScript (fetch)

JavaScript
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);
    
    if (response.status !== 429) {
      return response;
    }
    
    const retryAfter = response.headers.get('Retry-After');
    const waitSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
    
    if (attempt < maxRetries) {
      await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
    }
  }
  
  throw new Error('Max retries exceeded');
}

// Usage
fetchWithRetry('https://YOUR-API-HOSTNAME/api/public/users', {
  headers: { 'X-API-Key': 'rw_abcd1234...' }
})
  .then(r => r.json())
  .then(console.log);

.NET (HttpClient with Polly)

Install the Polly library for resilient HTTP calls with automatic retry and rate-limit handling.

Shell
# Package Manager
dotnet add package Polly

# .NET CLI
dotnet package add Polly
C#
using System;
using System.Net;
using System.Threading.Tasks;
using Polly;

var retryPolicy = Policy
    .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.TooManyRequests)
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: (attempt, outcome, context) =>
            outcome.Result?.Headers.RetryAfter?.Delta
            ?? (outcome.Result?.Headers.RetryAfter?.Date - DateTimeOffset.UtcNow)
            ?? TimeSpan.FromSeconds(60),
        onRetryAsync: (outcome, delay, attempt, context) =>
        {
            Console.WriteLine($"Rate limited. Waiting {delay.TotalSeconds}s before retry {attempt}");
            return Task.CompletedTask;
        });

// Usage with HttpClient
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-API-Key", "rw_abcd1234...");

var response = await retryPolicy.ExecuteAsync(
    async () => await httpClient.GetAsync("https://YOUR-API-HOSTNAME/api/public/users"));

See also

Reconnecting to server...

Please wait while we restore your connection

Connection lost

Retrying in seconds...

Unable to reconnect

The server connection has been lost.
You may need to log in again.

Session Paused

The server has temporarily paused your session.

Session Expired

Your session could not be resumed.
Please reload the page to continue.