Skip to main content

Rate Limits

Ledgerline applies per-route rate limiting on all externally-facing HTTP endpoints. Limits are enforced using a fixed-window counter backed by Valkey (distributed across all Render instances).

Limits by endpoint

EndpointKeyDefault limitWindow
POST /api/discord/interactionsIP + Discord user-id30 req/min60 s
POST /api/easypost/webhookSource IP60 req/min60 s

Discord interactions

Each key is <client-ip>:<discord-user-id> when a user-id is present in the interaction body, or <client-ip> alone for unsigned or pre-authentication probes. This gives per-user throttling while still capping raw IP traffic.

EasyPost webhook

EasyPost delivers events from a fixed IP range. The limit is generous (60/min) to accommodate normal batch deliveries, but protects against replay attacks.

Response format

A rate-limited request receives a 429 Too Many Requests response:

{
"error": "Too Many Requests",
"retryAfterSeconds": 60
}

The Retry-After header is also set to the window duration in seconds.

Body size limits

In addition to rate limiting, each endpoint enforces a maximum request body size:

EndpointMax body size
POST /api/discord/interactions64 KB
POST /api/easypost/webhook256 KB

Requests exceeding the limit receive 413 Payload Too Large:

{
"error": "Payload Too Large",
"maxBytes": 65536
}

Observability

Every rate-limit rejection emits a Datadog counter metric rate_limit.exceeded with tags:

  • endpoint — logical endpoint name (e.g. discord_interactions, easypost_webhook)
  • source — the key that was throttled (IP or IP:user-id)

Configuration

Rate limits are configurable via environment variables. See CONFIG.md for details.

VariableDescriptionDefault
RATE_LIMIT_DISCORD_PER_MINDiscord interactions limit per minute30
RATE_LIMIT_EASYPOST_PER_MINEasyPost webhook limit per minute60
RATE_LIMIT_TENANT_PER_MINTenant-level global limit per minute1000
RATE_LIMIT_DISABLEDDisable all limiters (tests only)false

Failure mode

If Valkey is unavailable, the rate limiter fails open — requests are allowed through and no 429 is returned. This preserves availability during cache outages. The global express.json body size limit (256 KB) still applies regardless of Valkey state.