ffiec-data-connect (Python)
Async and rate limits
AsyncCompatibleClient, RateLimiter, and the 2500-requests-per-hour rolling window. Back-off and retry semantics.
The REST API has a hard rate limit of 2,500 requests per hour per user on a rolling basis. This page covers how to pace requests, how the library’s async client works, and what to do when you get a 429.
The rolling window
“Rolling” means: at any given moment, you’ve made at most 2,500 requests in the previous 60 minutes. A burst of 2,500 calls followed by silence doesn’t reset instantly — requests “age out” of the window one hour after they happened.
Plan for ~2,400/hour effective, not 2,500. The remaining 100 is buffer for the fact that the FFIEC server and your client disagree about the exact window by a few seconds, and any disagreement causes a 429.
RateLimiter
from ffiec_data_connect import RateLimiter
limiter = RateLimiter(max_requests=2400, window_seconds=3600)
The limiter tracks request timestamps in memory. Call limiter.wait_if_needed()
before each request:
for rssd in rssds:
limiter.wait_if_needed()
collect_data(creds, rssd_id=rssd, ...)
If you’re already at budget, wait_if_needed() blocks until the oldest request in
the window ages out. This is intentionally simple — good enough for batch jobs.
The limiter is thread-safe. Share one instance across threads.
AsyncCompatibleClient
For async code, the built-in client wraps credentials + optional rate limiter + an HTTPX async session:
import asyncio
from ffiec_data_connect import AsyncCompatibleClient, RateLimiter
async def main():
limiter = RateLimiter(max_requests=2400, window_seconds=3600)
async with AsyncCompatibleClient(creds, rate_limiter=limiter) as client:
periods = await client.collect_reporting_periods_async(series="call")
rows = await client.collect_data_async(
rssd_id="480228",
reporting_period=periods[-1],
series="call",
output_type="list",
)
return rows
asyncio.run(main())
Key points:
- Use it as an async context manager (
async with). The exit closes the underlying HTTPX client cleanly. - Each
*_asyncmethod mirrors the sync equivalent with the same signature plusawait. - The rate limiter, if attached, is respected inside each call.
Concurrent fan-out
asyncio.gather() runs requests in parallel. The rate limiter serializes them as
needed so you don’t over-run the budget:
async with AsyncCompatibleClient(creds, rate_limiter=limiter) as client:
tasks = [
client.collect_data_async(
rssd_id=rssd, reporting_period=period, series="call",
output_type="list",
)
for rssd in rssds
]
results = await asyncio.gather(*tasks, return_exceptions=True)
Use return_exceptions=True on long batch jobs — one failing RSSD shouldn’t fail the
whole run. Then iterate and re-attempt just the failures.
Tuning concurrency
The right max_workers / parallelism depends on your network and CPU budget, not on
the FFIEC — the rate limiter already prevents over-firing. As a starting point:
- Sync threads: 4–8 workers.
- Async: 20–40 in-flight tasks is fine with an aggressive limiter.
Past a certain point more concurrency doesn’t help — you’re waiting on the rolling window either way.
Handling 429
If you still get RateLimitError, respect the retry_after attribute:
from ffiec_data_connect import RateLimitError
try:
rows = collect_data(creds, rssd_id=rssd, ...)
except RateLimitError as exc:
time.sleep(exc.retry_after or 60)
rows = collect_data(creds, rssd_id=rssd, ...)
A 429 from the server means either the rolling window actually filled up, or your
clock disagrees with theirs. Either way, the server’s Retry-After is authoritative.
See also
- Bulk download — patterns using the limiter.
- Troubleshooting § Slow response times.