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.

The following 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.

API order placement

For API trading, use pre-signed orders. Server-side signing may slow order ingestion. Follow the one-time setup and grant orders:place_signed for signed single-order and batch submissions.

The rate limits below apply regardless of how you sign. A request budget is not a guarantee of order-ingestion speed. The retry example on this page uses server-signed placement; the same backoff rules apply to pre-signed requests.

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 tierHigher tier
ReadEvery authenticated request debits this — see request costs.100 burst, refills at 50/sec750 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 150/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 350/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. An account on the Higher tier can place up to 32 × 20 = 640 orders/sec through it.

The individual Cancel budget sits above the single-order Place budget so pulling quotes one at a time has extra headroom. Cancel-all has its own lower bucket; prefer one cancel-all call over looping individual cancels because it is more efficient, not because its limit is higher. One request schedules cancellation for the orders visible when each wallet's sweep begins; keep reconciling the fully paginated order list until the relevant orders are terminal.

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
POST /trade/v1/portfolio/activities/list5 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 or SSE stream consume no request budget. Each connection attempt counts against that endpoint's stream attempt limit and the applicable per-IP edge caps, including unsuccessful attempts. Reconnect with backoff.

Higher tier

Every account starts on the Authenticated tier. The Higher tier provides increased request limits for accounts that need more throughput.

Request higher limits

If you need higher rate limits, email support@agara.xyz to request access to the Higher tier.

After your account moves to the Higher tier, the larger limits apply from your 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

Authenticated trading responses normally report the bucket you're closest to draining. Treat these headers as optional because some responses, including per-IP 429s, omit them:

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-Request-ID: 8051f907-4eeb-4ca9-bd84-b447ea65a68c
X-RateLimit-Bucket: place
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 10
Content-Type: application/problem+json

{
  "type": "urn:agara:problem:rate-limited",
  "title": "Too many requests",
  "status": 429,
  "code": "rate_limited",
  "request_id": "8051f907-4eeb-4ca9-bd84-b447ea65a68c",
  "recovery": {
    "strategy": "retry_after",
    "after_seconds": 1
  }
}
HeaderMeaning
Retry-AfterSeconds to wait before this request will fit.
X-Request-IDMatches request_id in the body; retain it when contacting support.
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.

Use the stable code for programmatic handling; the title is for display. recovery.after_seconds matches Retry-After.

See Errors and recovery for the shared Problem Details fields and how to handle unknown recovery strategies safely.

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
import random
import time

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/...)14,000 per minute — about 233 per second
Market data, events, prices, search (/api/v1/...)6,000 per minute — about 100 per second
Everything else (web pages, docs)16,000 per minute — about 267 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 is shared by all accounts using the same IP. A single Authenticated-tier account reaches its account limits before the trading edge cap. An account on the Higher tier 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.

On an edge 429, wait for the full Retry-After. The response uses the same rate_limited code, but omits the account-bucket X-RateLimit-* headers, X-Request-ID, and the body request_id:

HTTP/2 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "type": "urn:agara:problem:rate-limited",
  "title": "Too many requests",
  "status": 429,
  "code": "rate_limited",
  "recovery": {
    "strategy": "retry_after",
    "after_seconds": 60
  }
}

Public request and connection limits

On app.agara.xyz, ordinary HTTP requests share a burst budget per IP within each service. Switching endpoints or HTTP methods does not give you another service budget. Individual endpoints may also have a separate request limit. These limits apply in addition to the per-account and per-IP edge caps above, and operators can adjust them.

ServiceDefault burstDefault requests/sec
API (/api/v1/*)400400
Web pages and web-owned routes400400
Router (/trade/v1/*)1,0001,000

Responses served directly from the CDN cache do not spend this request budget. The seven trading-action endpoints listed above also have separate per-IP limits with the Higher-tier burst and refill values. Service, endpoint and account limits all apply; their budgets are not added together.

When this burst budget is exhausted, wait for Retry-After before retrying. The response uses application/problem+json, code rate_limited, and an X-Request-ID matching the body's request_id. It omits the account-bucket X-RateLimit-* headers. If admission is temporarily unavailable, you receive 503 with code dependency_unavailable; follow its retry instructions.

Stream connection attempts

Each endpoint below allows 300 connection attempts per IP per 60 seconds:

EndpointTransport
/api/v1/prices/streamSSE
/api/v1/prices/pyth-pro/streamSSE
/trade/v1/market-streamWebSocket
/trade/v1/account-streamWebSocket
/trade/v1/portfolio/streamSSE

Each endpoint has its own counter, shared by all accounts using the same IP. Query parameters and an optional trailing slash do not create another allowance. Unsuccessful attempts and reconnects count too. These five endpoints do not spend the service-level burst budgets above; the per-IP edge caps and any applicable account limits still apply.

This is an attempt limit, not a cap on simultaneous open connections or messages. Reusing an open connection spends no additional attempts. Closing one does not restore attempts already counted in the trailing window, and reaching the limit does not disconnect existing streams.

On a connection-attempt 429, wait the full Retry-After: 60, add jitter, then reconnect. The response uses application/json with code rate_limited, as in the edge response above. Counting has a brief settling delay, so do not depend on rejection at exactly attempt 301 or immediate recovery after slowing down.

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.