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
notifyAtLog a warning after waiting this many seconds10
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.