Both the TypeScript backend (comebackhere-backend) and the Rust backend
(backend) enforce per-IP rate limiting on every API endpoint. This page
documents the default limits, how to configure them, and the response shape
returned when a client exceeds the budget.
| Setting | Default | Environment variable |
|---|---|---|
| Max requests per window | 60 | RATE_LIMIT_POINTS |
| Window duration | 60 seconds | RATE_LIMIT_DURATION |
The same defaults apply to both backends. Operators can override them by setting the environment variables before starting the service.
The rate limiter is applied as global middleware — every endpoint listed in docs/api-reference.md is subject to the same per-IP budget. There is currently no per-route or per-user differentiation.
| Backend | Middleware layer |
|---|---|
comebackhere-backend (Express) |
rateLimitMiddleware in src/middleware/rateLimiter.ts |
backend (Axum / Tower) |
RateLimiterLayer in src/rate_limiter.rs |
The client IP is resolved in the following order:
X-Forwarded-Forheader — the first comma-separated entry is used. Leading and trailing whitespace is trimmed.- Peer socket address — the TCP connection's remote address.
"unknown"— fallback when neither source is available.
Note: Because the limiter is per-IP, all clients sharing the same public IP (e.g. behind a corporate NAT) share the same rate-limit bucket.
Every API response — whether the request succeeds or is rejected — includes the following headers so clients can proactively back off before hitting the limit:
| Header | Type | Description |
|---|---|---|
X-RateLimit-Limit |
integer | Total requests allowed per window. |
X-RateLimit-Remaining |
integer | Requests remaining in the current window. |
X-RateLimit-Reset |
integer | Unix timestamp (seconds) when the window resets. |
Example headers on a successful response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1720000060
When the limit is exceeded the backend returns HTTP 429 with the following shape:
{
"error": "Too many requests. Please retry after the indicated number of seconds.",
"retryAfter": 12
}| Field | Type | Description |
|---|---|---|
error |
string | Human-readable message. |
retryAfter |
number | Seconds to wait before retrying. |
The response also includes a Retry-After header with the same integer value,
plus X-RateLimit-Limit, X-RateLimit-Remaining: 0, and X-RateLimit-Reset.
RATE_LIMIT_POINTS=200 RATE_LIMIT_DURATION=60 node dist/app.jsRATE_LIMIT_POINTS=10 RATE_LIMIT_DURATION=60 node dist/app.js- Uses
rate-limiter-flexible. - When
REDIS_URLis set, rate-limit state is stored in Redis (key prefixrl:invoice) with an in-memory fallback if Redis is unreachable. - When
REDIS_URLis not set (local development and tests), the limiter runs entirely in memory.
- Implements a sliding-window algorithm as a
tower::Layer. - Stores per-IP buckets in an in-memory
HashMapprotected by aMutex. - Retains only timestamps that fall inside the current window, so the window rolls forward naturally without a background cleanup.
- docs/api-reference.md — full endpoint catalogue.
- docs/error-codes.md — contract-level error codes (distinct from HTTP 429).
- Issue #215 — rate-limiter test suite.