Skip to main content

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​

OptionDescriptionDefault
strategyHow to handle limits: pause, throttle, or fail (unquoted)pause
maxWaitMaximum time to wait, in seconds, before giving up300
fallbackRpmRequests per minute to assume when the API sends no rate limit headers60
modelA model of the server's own limiter, so throttle paces under it. See Modelling the server's limiternone

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 }
}
}
FieldDescriptionDefault
typeOnly tokenBucket is supported (unquoted). Anything else is a parse error—
capacityTokens a full bucket holds — the burst the server tolerates. Required—
refillTokens regained per second — the sustained rate. Required—
safetyPace 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:

  1. Pauses execution.
  2. Waits until the rate limit window resets (or maxWait seconds elapse, after which it throws).
  3. 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:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed
X-RateLimit-RemainingRequests remaining in window
X-RateLimit-ResetWhen the window resets
Retry-AfterSeconds 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:

  1. The rate limiter checks whether the request is allowed.
  2. If not, it pauses (based on strategy).
  3. The request is made.
  4. 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.