Rate limiting
Reqon includes an adaptive rate limiter that learns from API responses and respects standard rate limit headers.
Source-level configuration
Rate limiting is configured on a source, and applies to every request made through it:
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: pause
}
}
Rate limit options
| Option | Description | Default |
|---|---|---|
strategy | How to handle limits: pause, throttle, or fail (unquoted) | pause |
maxWait | Maximum time to wait, in seconds, before giving up | 300 |
notifyAt | Log a warning after waiting this many seconds | 10 |
fallbackRpm | Requests per minute to assume when the API sends no rate limit headers | 60 |
model | A model of the server's own limiter, so throttle paces under it. See Modelling the server's limiter | none |
There's no requestsPerMinute option, and there's no adaptive flag. The limiter is adaptive by default: it reads rate limit headers from each response and paces itself accordingly. fallbackRpm only kicks in when an API sends no headers.
The strategy value is an unquoted identifier (strategy: pause), not a string (strategy: "pause" is a parse error).
Modelling the server's limiter
fallbackRpm paces at a flat rate, which wastes whatever burst allowance the
server tolerates. When you know the shape of the server's limiter, describe it
with model: and the throttle strategy simulates that bucket locally — using the
burst, then holding the sustained rate:
source API {
auth: none,
base: "https://api.example.com",
rateLimit: {
strategy: throttle,
model: { type: tokenBucket, capacity: 5000, refill: 300, safety: 0.9 }
}
}
| Field | Description | Default |
|---|---|---|
type | Only tokenBucket is supported (unquoted). Anything else is a parse error | — |
capacity | Tokens a full bucket holds — the burst the server tolerates. Required | — |
refill | Tokens regained per second — the sustained rate. Required | — |
safety | Pace at safety * refill for headroom against clock skew. Range (0, 1] | 1 |
The configured refill is treated as a ceiling, not gospel. The model
self-calibrates: each observed 429 multiplies the lane's send interval (easing
the pace below the ceiling), and quiet time decays that penalty back up. That
matters most for headerless limiters, where a 429 is the only feedback the
client ever gets. The slowdown is capped so a burst of rejections can't strand a
lane.
Model state is tracked per lane, so a source using a proxy pool gets a separate bucket per egress IP.
Strategies
Pause strategy
Wait when the rate limit is reached:
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: pause
}
}
When the limit is reached, Reqon:
- Pauses execution.
- Waits until the rate limit window resets (or
maxWaitseconds elapse, after which it throws). - Continues with the next request.
Throttle strategy
Slow down requests proactively to stay under the limit:
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: throttle,
fallbackRpm: 60
}
}
Throttle spaces requests out. When the API reports remaining quota and a reset time, requests are spread evenly across the remaining window; otherwise fallbackRpm sets the pace.
Fail strategy
Throw an error when the limit is reached instead of waiting:
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: fail
}
}
Response header support
Reqon automatically reads standard rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed |
X-RateLimit-Remaining | Requests remaining in window |
X-RateLimit-Reset | When the window resets |
Retry-After | Seconds to wait before retrying |
RateLimit-* and X-Rate-Limit-* header variants are recognised too.
Header parsing
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705752000
Retry-After: 60
On a 429 with a Retry-After, Reqon waits for the indicated time (subject to maxWait), then continues. You don't need to hand-write retry logic for rate limits; the limiter handles the pause for you.
Per-endpoint tracking
The limiter tracks rate limit state per endpoint automatically, learning each endpoint's limit from its response headers. There's no per-request rate limit option. To apply different configured strategies, define separate sources:
mission APISync {
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: { strategy: pause }
}
source HeavyAPI {
auth: bearer,
base: "https://api.example.com",
rateLimit: { strategy: throttle, fallbackRpm: 10 }
}
action FetchUsers {
get "/users" { source: API }
}
action FetchReports {
get "/reports" { source: HeavyAPI }
}
}
Combining with pagination
get "/items" {
paginate: offset(offset, 100),
until: length(response.items) == 0
}
Rate limiting applies to each page request, not just the action as a whole.
Combining with retry
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: pause
}
}
action Fetch {
get "/data" {
retry: {
maxAttempts: 5,
backoff: exponential
}
}
}
Order of operations:
- The rate limiter checks whether the request is allowed.
- If not, it pauses (based on strategy).
- The request is made.
- If it fails, retry logic takes over.
Multiple sources with different limits
mission MultiSourceSync {
source HighVolumeAPI {
auth: bearer,
base: "https://high-volume.api.com",
rateLimit: { strategy: throttle, fallbackRpm: 1000 }
}
source LowVolumeAPI {
auth: bearer,
base: "https://limited.api.com",
rateLimit: { strategy: throttle, fallbackRpm: 10 }
}
action FetchBoth {
// Each source respects its own configuration
get "/items" { source: HighVolumeAPI }
get "/items" { source: LowVolumeAPI }
}
}
Best practices
Use pause for critical syncs
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: pause // Ensures completion, waiting when needed
}
}
Use throttle for background jobs
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: throttle,
fallbackRpm: 60 // Smooth, predictable pacing
}
}
Set a reasonable maxWait
maxWait is in seconds, so 300 is five minutes:
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: pause,
maxWait: 300 // Give up after 5 minutes of waiting
}
}
Combine with a circuit breaker
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: pause
},
circuitBreaker: {
failureThreshold: 5,
resetTimeout: 30000
}
}
Troubleshooting
Still hitting rate limits
If the API sends no rate limit headers, the limiter falls back to fallbackRpm. Lower it to slow down:
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: throttle,
fallbackRpm: 30
}
}
Requests too slow
If throttle is pacing too conservatively, switch to pause, which only waits when the limit is actually reached:
source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
strategy: pause
}
}
Inconsistent API limits
The limiter already adapts to response headers automatically, so you don't need to do anything special for APIs whose limits vary. If an API sends no headers at all, set fallbackRpm to a safe baseline.