FredAPIClient
A fully-typed Elixir client for the Federal Reserve Economic Data (FRED®) API.
Covers all 36 endpoints across 7 groups — Categories, Releases, Series, Sources, Tags, GeoFRED Maps, and bulk API v2 — with built-in Cachex caching, frequency-aware TTLs, and automatic retry on rate-limit errors.
Table of Contents
- Installation
- Configuration
- Quick Start
- Caching
- Rate Limiting
- Error Handling
- Multi-tenant / Explicit Config
- API Coverage
- License
Installation
Add fred_api_client to your mix.exs dependencies:
def deps do
[
{:fred_api_client, "~> 0.1"}
]
endThen fetch:
mix deps.getGet a free FRED API key at https://fred.stlouisfed.org/docs/api/api_key.html.
Configuration
Minimal
# config/runtime.exs ← recommended: keeps secrets out of compiled code
import Config
config :fred_api_client,
api_key: System.fetch_env!("FRED_API_KEY")Full reference
# config/config.exs
import Config
config :fred_api_client,
# ── Required ─────────────────────────────────────────────────────
api_key: System.get_env("FRED_API_KEY"),
# ── HTTP ─────────────────────────────────────────────────────────
base_url: "https://api.stlouisfed.org", # default
file_type: "json", # default — "json" | "xml"
timeout: 30_000, # default — milliseconds
# ── Caching (Cachex) ─────────────────────────────────────────────
cache_enabled: true, # default — set false to disable globally
cache_name: :fred_api_cache, # default — Cachex process name
# Optional: override individual TTL buckets (values in milliseconds).
# Only set the buckets you want to change — others keep their defaults.
ttl_overrides: %{
ttl_24h: :timer.hours(48), # categories, series metadata, sources, shapes
ttl_12h: :timer.hours(6), # release metadata, tags
ttl_6h: :timer.hours(3), # quarterly/annual observations, vintage dates
ttl_2h: :timer.hours(1), # GeoFRED series/regional data
ttl_1h: :timer.minutes(30) # monthly observations, release dates
},
# ── Rate Limiting ────────────────────────────────────────────────
# FRED enforces 120 requests/minute per API key.
# On HTTP 429 the client retries with exponential backoff:
# attempt 1 → wait base_delay × 1 (default 20 s)
# attempt 2 → wait base_delay × 2 (default 40 s)
# attempt 3 → wait base_delay × 3 (default 60 s) → give up
rate_limit_max_retries: 3, # default
rate_limit_base_delay_ms: 20_000 # defaultRuntime secrets (production)
# config/runtime.exs
import Config
if config_env() == :prod do
config :fred_api_client,
api_key: System.fetch_env!("FRED_API_KEY")
endQuick Start
# GDP quarterly observations — automatically cached for 6 h
{:ok, data} = FredAPIClient.get_series_observations(%{
series_id: "GDP",
observation_start: "2010-01-01",
units: "pc1",
frequency: "q"
})
IO.inspect(data["observations"])
# [%{"date" => "2010-01-01", "value" => "3.7"}, ...]
# Search for series (not cached — free-text results vary)
{:ok, results} = FredAPIClient.search_series(%{
search_text: "unemployment rate",
limit: 5,
order_by: "popularity",
sort_order: "desc"
})
# All releases (cached 12 h)
{:ok, releases} = FredAPIClient.get_releases(%{limit: 10})
# Category tree (cached 24 h)
{:ok, children} = FredAPIClient.get_category_children(%{category_id: 0})
# GeoFRED regional data (cached 2 h)
{:ok, geo} = FredAPIClient.get_regional_data(%{
series_group: "882",
region_type: "state",
date: "2023-01-01",
season: "NSA",
units: "Dollars"
})Caching
Caching is enabled by default via Cachex. The Cachex process is started automatically by the library’s OTP application — no setup required.
TTL strategy
TTLs are tuned to match FRED’s actual publication cadence, not arbitrary round numbers:
| Data type | TTL | Reason |
|---|---|---|
| Category tree | 24 h | Essentially static — structure never changes |
| Series metadata | 24 h | Title, units, frequency never change |
| Series categories / release / tags | 24 h | Static mappings |
| Release metadata, series, tables, tags | 12 h | Rarely changes |
| Release sources | 24 h | Source organisations never change |
| Tags vocabulary | 12 h | New tags are rare |
| GeoFRED shapes / series group | 24 h | Static geographic metadata |
| Observations — quarterly / semi-annual / annual | 6 h | Published ~4× per year |
| Series vintage dates | 6 h | List grows slowly |
| GeoFRED series / regional data | 2 h | Updated on release schedule |
| Observations — monthly | 1 h | Published monthly |
| Release dates | 1 h | Changes on publish schedule |
Not cached (volatile):
| Endpoint | Reason |
|---|---|
Series.search/2 | Free-text — results differ per query |
Series.get_updates/2 | Volatile by design |
Series.get_search_tags/2 / get_search_related_tags/2 | Query-dependent |
Tags.get_series/2 | Tag combination results vary |
V2.get_release_observations/2 | Large bulk payload |
Observations — d / w / bw and all weekly variants | Updated too frequently |
| Observations — unspecified frequency | Cannot determine volatility safely |
Manual cache control
The FredAPIClient.Cache module exposes the full cache API:
alias FredAPIClient.Cache
# Invalidate a single key
Cache.invalidate("fred:series:get_series:abc123")
# Invalidate an entire group by prefix
{:ok, deleted_count} = Cache.invalidate_prefix("fred:categories:")
{:ok, deleted_count} = Cache.invalidate_prefix("fred:series:")
# Clear everything
Cache.clear()
# Inspect cache size and status
{:ok, stats} = Cache.stats()
# %{hits: 142, misses: 38, evictions: 0, ...}
# Manually build a key (useful for targeted invalidation)
key = Cache.build_key("series", "get_series", %{series_id: "GDP"})
Cache.invalidate(key)Disabling the cache
Globally — in config/test.exs or wherever you don’t want caching:
config :fred_api_client, cache_enabled: falsePer-config call — pass an explicit config with cache_enabled: false (see
Multi-tenant / Explicit Config below):
config = %{api_key: "...", cache_enabled: false}
FredAPIClient.get_series_observations(%{series_id: "GDP"}, config)Overriding TTLs
You can tune any TTL bucket without changing code:
# config/config.exs
config :fred_api_client,
ttl_overrides: %{
ttl_1h: :timer.minutes(30), # halve the monthly-observations TTL
ttl_24h: :timer.hours(48) # cache static data for 2 days instead of 1
}Only the keys you specify are overridden — others stay at their defaults.
Rate Limiting
The FRED API enforces 120 requests per minute per API key. Exceeding this returns
HTTP 429 Too Many Requests.
The client handles 429 automatically with exponential backoff. The default of
3 retries with a 20 s base delay recovers safely within the 60 s rate-limit window:
| Attempt | Wait |
|---|---|
| 1st retry | 20 s |
| 2nd retry | 40 s |
| 3rd retry | 60 s |
| Give up | {:error, %FredAPIClient.HTTP.Error{code: 429}} |
503 Service Unavailable is also retried automatically (5 s base delay, shorter backoff).
Practical tip: For bulk data collection, enable caching (default) and batch your calls. A warm cache means most calls never hit the network, making the rate limit a non-issue in practice.
To tune retry behaviour:
config :fred_api_client,
rate_limit_max_retries: 5, # more retries for unreliable networks
rate_limit_base_delay_ms: 10_000 # shorter delay if you have burst headroomError Handling
All functions return {:ok, map()} or {:error, %FredAPIClient.HTTP.Error{}}:
case FredAPIClient.get_series_observations(%{series_id: "INVALID"}) do
{:ok, data} ->
IO.inspect(data["observations"])
{:error, %FredAPIClient.HTTP.Error{code: 400, message: message}} ->
Logger.warning("Bad request: #{message}")
{:error, %FredAPIClient.HTTP.Error{code: 429, message: message}} ->
Logger.error("Rate limit hit after all retries: #{message}")
{:error, %FredAPIClient.HTTP.Error{code: 408}} ->
Logger.error("Request timed out")
endFredAPIClient.HTTP.Error fields:
| Field | Type | Description |
|---|---|---|
code | integer | FRED API error code, or HTTP status code |
status | integer | nil |
HTTP status (nil for timeout / network errors) |
message | string | Human-readable description |
Multi-tenant / Explicit Config
Pass a config map as the second argument to use a different API key per call. This bypasses application config entirely:
config = %{
api_key: "tenant_specific_key",
timeout: 10_000,
cache_enabled: true,
rate_limit_max_retries: 2,
rate_limit_base_delay_ms: 5_000
}
FredAPIClient.get_series_observations(%{series_id: "GDP"}, config)All API modules also accept an explicit config directly:
FredAPIClient.API.Series.get_observations(%{series_id: "GDP"}, config)
FredAPIClient.API.Categories.get_category(%{category_id: 125}, config)API Coverage
All 36 endpoints, grouped by module:
| Module | Function | Endpoint | Cached |
|---|---|---|---|
FredAPIClient.API.Categories | get_category/2 | GET /fred/category | ✅ 24 h |
get_children/2 | GET /fred/category/children | ✅ 24 h | |
get_related/2 | GET /fred/category/related | ✅ 24 h | |
get_series/2 | GET /fred/category/series | ✅ 24 h | |
get_tags/2 | GET /fred/category/tags | ✅ 24 h | |
get_related_tags/2 | GET /fred/category/related_tags | ✅ 24 h | |
FredAPIClient.API.Releases | get_releases/2 | GET /fred/releases | ✅ 12 h |
get_all_release_dates/2 | GET /fred/releases/dates | ✅ 1 h | |
get_release/2 | GET /fred/release | ✅ 12 h | |
get_release_dates/2 | GET /fred/release/dates | ✅ 1 h | |
get_release_series/2 | GET /fred/release/series | ✅ 12 h | |
get_release_sources/2 | GET /fred/release/sources | ✅ 24 h | |
get_release_tags/2 | GET /fred/release/tags | ✅ 12 h | |
get_release_related_tags/2 | GET /fred/release/related_tags | ✅ 12 h | |
get_release_tables/2 | GET /fred/release/tables | ✅ 12 h | |
FredAPIClient.API.Series | get_series/2 | GET /fred/series | ✅ 24 h |
get_categories/2 | GET /fred/series/categories | ✅ 24 h | |
get_observations/2 | GET /fred/series/observations | ⚠️ by freq | |
get_release/2 | GET /fred/series/release | ✅ 24 h | |
search/2 | GET /fred/series/search | ❌ | |
get_search_tags/2 | GET /fred/series/search/tags | ❌ | |
get_search_related_tags/2 | GET /fred/series/search/related_tags | ❌ | |
get_tags/2 | GET /fred/series/tags | ✅ 24 h | |
get_updates/2 | GET /fred/series/updates | ❌ | |
get_vintage_dates/2 | GET /fred/series/vintagedates | ✅ 6 h | |
FredAPIClient.API.Sources | get_sources/2 | GET /fred/sources | ✅ 24 h |
get_source/2 | GET /fred/source | ✅ 24 h | |
get_source_releases/2 | GET /fred/source/releases | ✅ 24 h | |
FredAPIClient.API.Tags | get_tags/2 | GET /fred/tags | ✅ 12 h |
get_related_tags/2 | GET /fred/related_tags | ✅ 12 h | |
get_series/2 | GET /fred/tags/series | ❌ | |
FredAPIClient.API.Maps | get_shapes/2 | GET /geofred/shapes/file | ✅ 24 h |
get_series_group/2 | GET /geofred/series/group | ✅ 24 h | |
get_series_data/2 | GET /geofred/series/data | ✅ 2 h | |
get_regional_data/2 | GET /geofred/regional/data | ✅ 2 h | |
FredAPIClient.API.V2 | get_release_observations/2 | GET /fred/v2/release/observations | ❌ |
⚠️ = frequency-aware: m → 1 h, q/sa/a → 6 h, d/w/bw → not cached
License
MIT — see LICENSE.txt.