ffiec-data-connect (Python)
Bulk download
Patterns for pulling many institutions or many periods without tripping the 2500/hour rate limit.
The REST API serves one institution’s facsimile per call. For multi-institution work you’ll make many calls — the shape of your loop determines whether you finish in minutes or hit a 429.
The rate budget
The REST API allows 2,500 requests per hour per user on a rolling basis. Every
collect_data(...) call and every collect_ubpr_facsimile_data(...) call counts as
one request. collect_filers_on_reporting_period(...) and
collect_reporting_periods(...) count too.
Back-of-envelope: pulling one period for 5,000 banks at sequential pace hits the limit hard. You need either pacing, async concurrency, or both.
Pattern 1: synchronous with pacing
Simple, slow, never 429s:
import time
from ffiec_data_connect import OAuth2Credentials, collect_data, collect_filers_on_reporting_period
creds = OAuth2Credentials(...)
period = "12/31/2025"
# Get the full panel
panel = collect_filers_on_reporting_period(creds, reporting_period=period, series="call")
rssds = [p["id_rssd"] for p in panel]
# ~1.5 seconds per call = ~2400/hour, safely under 2500
interval = 1.5
results = []
for rssd in rssds:
results.append(collect_data(creds, rssd_id=rssd, reporting_period=period, series="call",
output_type="list"))
time.sleep(interval)
Best for: one-off ad-hoc pulls, low-volume scripts, demos.
Pattern 2: RateLimiter + threads
ffiec_data_connect.RateLimiter wraps a rolling-window counter. Combine it with a
thread pool for concurrency without 429s.
from concurrent.futures import ThreadPoolExecutor, as_completed
from ffiec_data_connect import RateLimiter, collect_data
limiter = RateLimiter(max_requests=2400, window_seconds=3600) # under the 2500 ceiling
def fetch(rssd: str) -> list[dict]:
limiter.wait_if_needed()
return collect_data(creds, rssd_id=rssd, reporting_period=period, series="call",
output_type="list")
with ThreadPoolExecutor(max_workers=8) as pool:
futures = {pool.submit(fetch, r): r for r in rssds}
for fut in as_completed(futures):
rssd = futures[fut]
try:
rows = fut.result()
# ... persist rows
except Exception as exc:
print(f"{rssd}: {exc}")
Best for: batch pipelines, backfills, anywhere you have 1–several thousand RSSDs.
Pattern 3: AsyncCompatibleClient
For async codebases (FastAPI, Prefect, etc.), use the built-in client:
import asyncio
from ffiec_data_connect import AsyncCompatibleClient, RateLimiter
async def run():
limiter = RateLimiter(max_requests=2400, window_seconds=3600)
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
]
return await asyncio.gather(*tasks, return_exceptions=True)
results = asyncio.run(run())
The client handles pacing internally when given a RateLimiter. See
Async and rate limits for the full treatment.
Storing results
If you’re pulling many periods × many banks, don’t hold it all in memory. Stream to disk as you go.
Parquet per (period, rssd) works well for large panels:
import pandas as pd
from pathlib import Path
out = Path("data/call")
out.mkdir(parents=True, exist_ok=True)
for rssd in rssds:
rows = collect_data(creds, rssd_id=rssd, reporting_period=period, series="call",
output_type="list")
pd.DataFrame(rows).to_parquet(out / f"{period.replace('/', '-')}_{rssd}.parquet")
When to use bulk ZIPs instead
The FFIEC CDR publishes per-quarter bulk ZIPs containing every institution’s facsimile. If your scope is the entire industry for one quarter, the bulk ZIP is almost always faster than iterating the REST API. See Bulk files.
Use the REST API when your scope is a specific set of institutions or the latest amendments for an institution you already track.
See also
- Async and rate limits — inside the limiter.
- Incremental updates — only pull what changed.
- Bulk files — the non-API path.