Call.Report
GitHub
GitHub

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 *_async method mirrors the sync equivalent with the same signature plus await.
  • 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

navigate · open · Esc close