All articles
Starter Guide

Getting Started with FRED API

First pull, key series, and what to expect when building pipelines on the Federal Reserve Economic Data API.

FRED API Federal Reserve time series GDP CPI unemployment Python macroeconomics ALFRED vintage data

Source on EconIndx: Federal Reserve Economic Data (FRED) — free, no enterprise tier, 800,000+ time series.

Access & Pricing

FRED is completely free. Get an API key at fredaccount.stlouisfed.org — email registration, instant activation. No contracts, no approval. The API key raises your rate limit from 30 to 120 requests/minute. No enterprise tier exists; everyone gets the same access.

Your First Data Pull

Install nothing special — requests is enough. Register your key and pull your first series in under two minutes:

import requests
import pandas as pd

FRED_KEY = "your_api_key_here"

def fetch_series(series_id: str, start: str = "2000-01-01") -> pd.DataFrame:
    r = requests.get(
        "https://api.stlouisfed.org/fred/series/observations",
        params={
            "series_id": series_id,
            "api_key": FRED_KEY,
            "file_type": "json",
            "observation_start": start,
            "sort_order": "asc",
            "limit": 10000,
        }
    )
    obs = r.json()["observations"]
    df = pd.DataFrame(obs)[["date", "value"]]
    df["value"] = pd.to_numeric(df["value"], errors="coerce")  # "." = missing
    df["series_id"] = series_id
    return df

# Three essential series to start
gdp    = fetch_series("GDP")        # Quarterly US GDP, billions USD
cpi    = fetch_series("CPIAUCSL")   # Monthly CPI, all urban consumers
unrate = fetch_series("UNRATE")     # Monthly unemployment rate, %

print(f"GDP rows: {len(gdp)}, from {gdp.date.min()} to {gdp.date.max()}")
print(f"CPI rows: {len(cpi)}, from {cpi.date.min()} to {cpi.date.max()}")

First Pull: What to Expect

When you pull these three series you should see:

SeriesFrequencyExpected rows (from 2000)History available
GDPQuarterly~100Back to 1947 Q1
CPIAUCSLMonthly~300Back to 1947
UNRATEMonthly~300Back to 1948
DGS10 (10yr Treasury)Daily~6,500Back to 1962

A full backfill of all three (no date filter) completes in under 5 seconds at the default rate limit. Missing values come back as "." — always coerce to numeric and handle nulls.

Key Datasets to Start With

Macro indicators:

  • GDP — nominal GDP, quarterly, billions USD
  • GDPC1 — real GDP (inflation-adjusted)
  • CPIAUCSL — Consumer Price Index, seasonally adjusted
  • CPILFESL — Core CPI (excludes food & energy)
  • FEDFUNDS — Federal funds rate, daily

Labor market:

  • UNRATE — unemployment rate, monthly SA
  • PAYEMS — total nonfarm payrolls, monthly
  • ICSA — initial jobless claims, weekly

Financial:

  • DGS10 — 10-year Treasury yield, daily
  • DEXUSEU — USD/EUR exchange rate, daily
  • M2SL — M2 money supply, monthly

Data Tolerance & Validation

What’s normal:

  • Revisions are routine. GDP gets revised twice in the two months following initial release. Payrolls get revised monthly for the prior two months. Always re-pull the last 3 months on each load.
  • Missing values (".") are expected, especially at the start and end of a series. For UNRATE, expect no nulls after 1948. For newer experimental series, expect sparse early data.
  • Seasonal adjustment: every SA series has an NSA counterpart (e.g., UNRATENSA). Store which version you’re using in your metadata.

⚠️ Revision note: FRED data is revised frequently. GDP gets two preliminary revisions in the months following initial release. Payrolls (PAYEMS) are revised monthly for the prior two months. Always re-pull the last 3 months of any monthly or quarterly series on each pipeline run.

Validation checks to build:

def validate_fred_pull(df: pd.DataFrame, series_id: str) -> dict:
    null_rate = df["value"].isna().mean()
    latest_date = pd.to_datetime(df["date"]).max()
    days_stale = (pd.Timestamp.today() - latest_date).days

    return {
        "series_id": series_id,
        "row_count": len(df),
        "null_rate": round(null_rate, 4),
        "latest_date": str(latest_date.date()),
        "days_stale": days_stale,
        "stale_alert": days_stale > 60,  # monthly series; alert if >60 days old
    }

Alert thresholds to set:

  • UNRATE / PAYEMS / CPIAUCSL: alert if latest date is more than 45 days ago (monthly release, should update within 2 weeks of month end)
  • GDP: alert if latest date is more than 120 days ago (quarterly)
  • DGS10 / FEDFUNDS: alert if latest date is more than 5 business days ago (daily series)
  • Null rate above 5% for a core series: investigate

Schema Stability

FRED’s schema is extremely stable. Series IDs don’t change. The response envelope (observations, date, value) hasn’t changed in over a decade. The only breaking change pattern to watch: discontinued series return discontinued=true in the series metadata endpoint — worth checking when you add a new series to your registry.

💡 Tip: For point-in-time (vintage) analysis, use the ALFRED endpoint (https://alfred.stlouisfed.org/) with realtime_start and realtime_end parameters. This lets you reconstruct what the data looked like at any historical date — essential for backtesting models that would have been built on real-time data.

For vintage/point-in-time data, use the ALFRED endpoint with realtime_start and realtime_end parameters — same API, different endpoint base.

Batch Loading Multiple Series

import json

def fetch_batch(series_ids: list[str], start_year: str, end_year: str, key: str) -> list:
    """FRED supports up to 1 series per request — batch manually."""
    all_data = []
    for sid in series_ids:
        df = fetch_series(sid, start=f"{start_year}-01-01")
        all_data.append(df)
    return pd.concat(all_data, ignore_index=True)

series_to_load = ["GDP", "GDPC1", "CPIAUCSL", "UNRATE", "PAYEMS", "FEDFUNDS", "DGS10"]
master_df = fetch_batch(series_to_load, "2000", "2026", FRED_KEY)
print(f"Total rows loaded: {len(master_df)}")
# Expected: ~2,500–3,500 rows depending on history depth

Learn

Recent articles

View all →