Agara

Rate limits

How many requests you can make, what happens when you exceed, and how to build a client that backs off cleanly.

The API caps how many requests a single client can make. Caps exist to keep the service responsive for everyone. Most clients never come close to hitting them.

Two layers of caps compose, and they work differently:

  • Per-account caps on the trading API (/trade/v1/...) are token buckets: you can spend up to a bucket's burst budget instantly, and tokens return at a steady refill rate. They're tied to your account — every request you authenticate is metered, regardless of which IP you call from.
  • Per-IP caps at the edge are sliding windows: total requests from one IP over the trailing 60 seconds, counted on every request, authenticated or not. A safety net that mainly catches misbehaving bots and unauthenticated abuse.

Per-account caps

Caps apply per user account (not per token — if you mint multiple personal access tokens, they share one envelope).

Five independent buckets. For each, you can spend up to the burst instantly; tokens come back at the refill rate, capped at the burst.

BucketWhat it coversAuthenticated tierMarket-maker tier
ReadEvery authenticated request debits this — see request costs.100 burst, refills at 50/sec200 burst, refills at 500/sec
PlaceSingle order placement (POST /trade/v1/orders and POST /trade/v1/orders/signed).100 burst, refills at 10/sec500 burst, refills at 100/sec
Place-batchBatched pre-signed orders, account batches, and batch supersedes. One token per call, whatever the batch size.10 burst, refills at 1/sec100 burst, refills at 20/sec
CancelCancelling one order (DELETE /trade/v1/orders/{order_id}).200 burst, refills at 25/sec2,000 burst, refills at 250/sec
Cancel-allCancelling everything open (POST /trade/v1/orders/cancel-all).10 burst, refills at 1/sec10 burst, refills at 1/sec

The batch bucket is separate so a ladder re-quote can't drain your single-order Place budget. Because one batch call costs one token no matter how many orders it carries (up to 32), batching is far cheaper on your budget than the same orders sent one at a time — a market-maker-tier account can place up to 32 × 20 = 640 orders/sec through it.

Cancel budgets sit well above place budgets on purpose: pulling your quotes to reduce risk should never be the thing that gets throttled. When you're flattening all your exposure, prefer one cancel-all call over looping individual cancels — a single request removes every resting order you have.

Request costs

Most requests cost 1 token. The exceptions, exactly:

RequestCost
GET /trade/v1/portfolio/summary5 Read
POST /trade/v1/portfolio/positions/list5 Read
POST /trade/v1/portfolio/open-orders/list5 Read
GET /trade/v1/portfolio/trades5 Read
GET /trade/v1/portfolio/activities5 Read

Everything else costs 1. Place, place-batch, cancel, and cancel-all requests debit 1 token from their own bucket plus their Read cost — Read meters your total request rate, the others meter the specific action. A batch call debits exactly one Place-batch token regardless of how many orders it carries, even if some of those orders are rejected.

What counts against what

  • Rejected orders still count. An order that fails validation or bounces on insufficient balance consumed a Place token getting there. The buckets meter requests, not successful orders.
  • A 429 does not count. A request turned away with 429 was not processed and consumed nothing from the bucket that rejected it, so it's always safe to resend the same request after backing off — including POST /trade/v1/orders. A 429 means the order was never placed. One caveat: every trading request also spends from your read budget on the way in, and that spend happens even when the place or cancel bucket then turns the request away — a tight retry loop that keeps hitting 429s will still drain read, so back off rather than hammer.
  • Streaming messages are free; connecting is not. Messages over an open WebSocket consume nothing. Opening a connection counts as one request against the per-IP trading cap below, so a client stuck in a tight reconnect loop will eventually be blocked at the edge — reconnect with backoff.

Market-maker tier

Every account starts at the Authenticated tier. If your account is assigned the Market Maker tier, the larger limits apply from the next request; no new token or sign-in is required. Treat the returned headers as authoritative instead of assuming a tier from client configuration.

Response headers

Every authenticated response reports the bucket you're closest to draining:

X-RateLimit-Bucket: read
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 98
X-RateLimit-Reset: 1
HeaderMeaning
X-RateLimit-BucketThe bucket closest to empty.
X-RateLimit-LimitBucket capacity (the burst budget).
X-RateLimit-RemainingTokens left right now.
X-RateLimit-ResetSeconds until the bucket is fully refilled.

When a request trips a bucket you get a 429 Too Many Requests:

HTTP/2 429 Too Many Requests
Retry-After: 1
X-RateLimit-Bucket: place
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 10
Content-Type: application/json
 
{"error":"rate_limited"}
HeaderMeaning
Retry-AfterSeconds to wait before this request will fit.
X-RateLimit-BucketWhich bucket tripped: read, place, place_batch, cancel, or cancel_all.
X-RateLimit-LimitCapacity of the bucket that tripped.
X-RateLimit-RemainingTokens left in that bucket right now.
X-RateLimit-ResetSeconds until that bucket is back to full.

Retry-After and X-RateLimit-Reset answer different questions: wait Retry-After seconds and this one request fits again; wait X-RateLimit-Reset seconds and your full burst budget is back.

Handling a 429

Wait the full Retry-After, add a little jitter, then resend the identical request:

import os
 
for attempt in range(MAX_RETRIES):
    response = session.post(f"{os.environ['AGARA_BASE_URL']}/trade/v1/orders", json=order)
    if response.status_code != 429:
        break
    wait = int(response.headers.get("Retry-After", "1"))
    time.sleep(wait + random.uniform(0, 0.5))

Retrying a rate-limited order placement is safe: the 429 means the request was not processed, so resending it cannot double-place the order.

Per-IP edge caps

These apply before any account lookup, on every request. Each cap is a sliding window — total requests from your IP over the trailing 60 seconds:

What you're callingCap
Trading, including stream connections (/trade/v1/...)3,000 per minute — about 50 per second
Market data, events, prices, search (/api/v1/...)6,000 per minute — about 100 per second
Everything else (web pages, docs)12,000 per minute — about 200 per second

The two specific caps apply on top of the catch-all, so a burst of /api/v1/ calls counts against both the API cap and the overall cap.

The per-IP trading cap matches the standard tier's sustained request rate, so for a single Authenticated-tier account on one IP the account buckets are what you hit in practice — the edge only comes into play if you hold every bucket at its absolute maximum for a full minute. A market-maker-tier account running at full rate through a single IP can reach the edge cap first — if that's your setup, mention it when you request the tier and we'll plan around it with you.

Edge counting has a brief settling delay: a sudden spike may slip through for a few seconds before blocking starts, and may take a few seconds to clear after you slow down. Treat the cap as an average over the window, not an instantaneous limit, and build your client to back off on 429 rather than ride right up to the line.

An edge-generated 429 carries Retry-After: 60 but does not carry the account-bucket X-RateLimit-* headers. Handle Retry-After even when the bucket headers are absent.

Designing a client that stays well under

  • Watch the response headers. X-RateLimit-Remaining tells you exactly how much budget you have left. Slow down before you hit zero.
  • Back off on 429. Respect the full Retry-After window. Immediate retries keep you blocked and waste your budget further.
  • Poll slowly. Market data changes by the second, not the millisecond. A poll every 1-5 seconds is plenty for most use cases.
  • Use streaming for live updates. The orderbook and account event streams push changes over WebSocket as they happen — one request to connect, nothing after that. See Streaming and Account stream.
  • Flatten with cancel-all. One cancel-all beats hundreds of individual cancels, for you and for the book.

If you're behind a shared IP

The per-IP edge caps are per source IP. If your application calls the API from a shared egress — corporate network, NAT gateway, a serverless function pool with a small IP range — every caller behind that network shares the per-IP cap. The per-account caps above are unaffected by shared IPs.

In practice the per-IP caps almost never bite because they're well above normal usage. If you're operating from an egress that genuinely needs more headroom, get in touch.

Testing changes

Test backoff and retry changes with test funds before sending traffic to this deployment.

Treat headers as authoritative

Do not hardcode the table as a permanent contract. Use the returned limit, remaining, reset, and retry headers so your client follows the limits active for its account.

On this page