alpaca-broker-rate-limits-resilience
About alpaca-broker-rate-limits-resilience
A language-agnostic skill for building resilient Alpaca API clients, aimed at developers running bulk jobs, backfills, and reconciliation. It focuses on rate-limit-aware retries, bounded concurrency, pagination, and transport safeguards.
Key features
- 01Rate-limit header handling
- 02Retry and jittered backoff
- 03Bounded worker pools
- 04Paginated request loops
- 05Timeout and connection reuse
- 06Bulk-job resilience checklist
Use cases
- 01Running Alpaca backfills
- 02Reconciling large account batches
- 03Handling throttled API requests
- 04Maintaining resilient cron jobs
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ alpacahq/ alpaca-skills --skill alpaca-broker-rate-limits-resilienceAfter installing, type/alpaca-broker-rate-limits-resiliencein Claude Code to confirm it loaded.
Bundled filesreference.md
Details
From the project READMEAlpaca — Rate Limits & Resilience
Alpaca's APIs are rate-limited and occasionally flaky under load. Any client that does more than a handful of calls — especially bulk jobs, backfills, and reconciliation sweeps — needs disciplined retry, backoff, and concurrency control. These patterns are transport-level and apply in any language.
Read
alpaca-broker-integrationfirst.
1. Rate-limit headers — read them on every response
Alpaca returns standard headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | requests allowed in the window |
X-RateLimit-Remaining | requests left in the current window |
X-RateLimit-Reset | unix timestamp (seconds) when the window resets |
Parse them on every response, not just on errors. Two uses:
- Proactive: slow down as
Remainingfalls toward zero, rather than waiting to be throttled. Any warning threshold you pick is your own starting point, derived fromLimit— Alpaca publishes none. - Reactive: on
429, useResetto wait exactly until the window opens.
Broker limits are applied at the correspondent level — across your whole integration, not per end-user account, so every process you run shares one budget. They're set per partner and not published; react to the headers.
Sandbox limits are significantly lower than production and are not a reliable signal of production headroom. Alpaca explicitly says not to load-test against sandbox — mock the Alpaca calls instead and have the stub simulate latencies sampled from real production calls.
2. The retry loop (pseudocode)
MAX_ATTEMPTS = 10
INITIAL_DELAY_MS = 1000
MAX_DELAY_MS = 60000
backoff(n) = min(INITIAL_DELAY_MS * 2^(n-1), MAX_DELAY_MS) + random(0, 500)
for attempt in 1..MAX_ATTEMPTS:
res = http(request) # with a sane timeout (see §5)
limit, remaining, reset_at = parse_rate_headers(res.headers)
if remaining < low_water(limit): log_warn("approaching rate limit", reset_at)
if res.status == 429:
# wait until the window resets, plus a small buffer
wait = (reset_at - now()) if reset_at else backoff(attempt)
sleep(max(0, wait) + 1000 + random(0, 500))
continue
if res.status in (500, 502, 503, 504) or network_error:
sleep(backoff(attempt))
continue
return res # success or non-retryable 4xx
raise last_error
Key points:
- On
429, wait untilX-RateLimit-Reset+ a ~1s buffer, then keep exponential backoff underneath —Resetsays when the window rolls, not whether the herd behind you re-saturates it. - Exponential backoff (
base * 2^(attempt-1)) for network errors and 5xx, capped (the docs' example caps at 60s). With base 1s and 10 attempts the tail is minutes — fine for background jobs, too slow for user-facing calls (use fewer attempts there). - Jitter every wait. It's part of the documented recommendation, not an extra: a correspondent-wide limit means all your workers are throttled together and would otherwise retry in lockstep.
- Don't retry non-retryable 4xx (
400/403/422) — those won't fix themselves; surface them.
3. Bounded concurrency
Parallelism speeds bulk jobs but is the fastest way to hit limits. Use a fixed worker pool, not unbounded fan-out.
- Size the pool from
X-RateLimit-Limit, not from a fixed number, and remember the limit is correspondent-level: every worker, cron job, and web process you run draws on the same budget. Tune down from observedRemaining; your own downstream store is often the tighter constraint. - Cache per-entity reads within a run (e.g. an account's buying power, or a per-account transfer list) so you don't refetch the same thing across items in a batch.
- For per-item throttling, a small fixed sleep between calls (e.g. 100ms) is a crude-but-effective floor when you can't easily coordinate a pool.
4. Pagination loops
List endpoints page forward with a token — never assume one response is complete.
- Activities (
/v1/accounts/activities): the response is a bare JSON array with no pagination header — pass theidof the last activity in the page back aspage_tokenand stop when a page comes back short.page_sizedefaults to and maxes at 100 (no maximum whendateis set — it returns everything);directionisasc/desc, defaultdesc. - Market-data bars (
/v2/stocks/bars): page vianext_page_tokenin the body → pass back aspage_token. Rememberlimitcounts across all symbols and results sort by symbol-then-time, so a single page may contain only the first symbol(s) — keep paging. - Wrap each page fetch in the retry loop from §2.
token = null
loop:
page = fetch(url + (token ? "&page_token="+token : "")) # via retry loop
accumulate(page.items)
if body_token_endpoint:
token = page.next_page_token
if not token: break
else: # activities: bare array, no token
if len(page.items) < page_size: break
token = page.items[-1].id
5. Timeouts & batch sizing
- Always set an HTTP timeout (15–30s is a reasonable starting point, not an Alpaca number). A hung connection without a timeout stalls a whole worker pool. (SSE streams are the exception — they're meant to stay open; see
alpaca-broker-sse-events.) - Use a shared HTTP client / connection pool rather than constructing one per request, so keep-alive and connection reuse work.
- Detect silence on the market-data WebSocket and reconnect + resubscribe. A slow client is disconnected, and the
407 slow clienterror is "not guaranteed to arrive before you are disconnected" — so a dead socket can look identical to a quiet one (seealpaca-broker-market-data). - When writing reconciliation results to your own store, chunk bulk inserts to stay under DB statement-size limits and keep transactions reasonable; pick the chunk size from your own database's behavior.
6. Resilience checklist for a bulk/cron job
- Rate-limit headers parsed every response; proactive warn near the limit.
-
429→ wait untilX-RateLimit-Reset+ buffer. - Exponential backoff with jitter for 5xx/network; capped delay and capped attempts.
- Non-retryable 4xx surfaced, not retried.
- Bounded worker pool; per-run caching of repeated reads.
- Pagination loop until the token is empty.
- HTTP timeout on every call; shared client.
- Bulk DB writes chunked and idempotent (upsert) — see
alpaca-broker-reconciliation-idempotency. - Structured logging with a trace/correlation ID per item for debugging partial failures.
Related skills: safe re-runs of jobs → alpaca-broker-reconciliation-idempotency; the heal/poll jobs that use these patterns → alpaca-broker-reconciliation-idempotency; market-data pagination specifics → alpaca-broker-market-data.