-
Notifications
You must be signed in to change notification settings - Fork 0
Rate Limits
Cryptohopper applies per-bucket rate limits server-side. When you cross a threshold the server returns 429 Too Many Requests with a Retry-After header.
| Bucket | Limit | Endpoints |
|---|---|---|
normal |
30 requests / minute | Most reads (hoppers.list, user.get, etc.) |
order |
8 requests / 8-second window |
hoppers.buy, hoppers.sell
|
backtest |
1 request / 2 seconds |
backtest.create, backtest.get, … |
Each bucket has its own counter. Hitting the order limit doesn't slow down your normal reads.
By default maxRetries is 3. On a 429 the SDK:
- Reads the
Retry-Afterheader (in seconds — converted to ms internally). - Sleeps for that duration via
Task.sleep. - Retries the same request.
- Bubbles the error after
maxRetriesattempts as aCryptohopperErrorwithcode == .rateLimitedandretryAfterMspopulated.
Backoff isn't exponential — Retry-After is the server's authoritative wait. Doubling on top of it just makes you slower without reducing load (the server already accounted for fairness when picking the value).
do {
_ = try await client.hoppers.list()
} catch let e as CryptohopperError where e.code == .rateLimited {
print("Hit the limit \(e.retryAfterMs ?? 0)ms after \(3) retries.")
}let client = try Client(
apiKey: ProcessInfo.processInfo.environment["CRYPTOHOPPER_TOKEN"]!,
maxRetries: 0
)
do {
_ = try await client.hoppers.list()
} catch let e as CryptohopperError where e.code == .rateLimited {
// Handle 429 yourself — common when you have your own queue
// or are running inside a workflow engine that already retries.
try await Task.sleep(nanoseconds: UInt64(e.retryAfterMs ?? 1000) * 1_000_000)
}Backtests have an explicit quota endpoint:
if let limits = try await client.backtest.limits() as? [String: Any] {
print("Backtests remaining: \(limits["remaining"] ?? "?") of \(limits["limit"] ?? "?")")
}For normal and order, the only signal is the Retry-After header on 429.
The normal and order buckets are per-OAuth-token by default. If you pass appKey: <your_client_id> at construction time, the server applies a separate per-app bucket — so your app doesn't share rate-limit budget with every other unattributed caller.
let client = try Client(
apiKey: ProcessInfo.processInfo.environment["CRYPTOHOPPER_TOKEN"]!,
appKey: ProcessInfo.processInfo.environment["CRYPTOHOPPER_APP_KEY"]
)This is the same pattern in every Cryptohopper SDK.
If you fan out many requests at once (say, polling positions for 100 hoppers), you'll spike past the bucket faster than retries can absorb. Cap concurrency with a semaphore or a TaskGroup:
let semaphore = AsyncSemaphore(value: 10)
await withTaskGroup(of: Void.self) { group in
for h in hoppers {
group.addTask {
await semaphore.withPermit {
_ = try? await client.hoppers.positions(h["id"] as! String)
}
}
}
}(AsyncSemaphore is in swift-async-algorithms or similar — pick the one that fits your dep budget.)
For a copy-paste version using only stdlib, see Recipes#fan-out-with-taskgroup.
In an AWS Lambda-style invocation (15s) the default 30-second SDK timeout outlives the worker, leading to confusing "function killed" errors instead of clean .timeout errors.
let client = try Client(
apiKey: token,
timeout: 8, // ~half your function budget
maxRetries: 1 // leave room for one retry inside the function lifetime
)-
Error Handling — the
.rateLimitedcode in the broader taxonomy - Recipes — copyable patterns including bounded fan-out and custom retry wrappers
Pages
Other SDKs
Resources