How to download end-of-day prices for thousands of tickers at once
Ask for a date, not a ticker. A grouped-daily or bulk endpoint returns the whole market in one call; a yfinance loop returns 429s and quietly empty columns.
Stop looping over tickers and ask for a date. Massive's grouped daily endpoint, EODHD's eod-bulk-last-day, FMP's eod-bulk and Databento's EQUS.SUMMARY each return every US stock's bar for one day in one request, so a year is about 250 calls however many tickers you follow. Backfill history per symbol, or as one table from Sharadar. Then check adjustment, the date you actually got, delisted names, and a licence that is personal on most of these plans.
The short way
Turn the loop inside out. A ticker loop asks for each symbol's history, one request per symbol; a bulk endpoint asks for one date and returns every symbol's bar for it. A thousand tickers over a year is a thousand history requests in the first shape and about 250 in the second, and the second does not grow when the universe does. Massive's grouped daily endpoint is the version you can run on a free key:
import io
import time
import pandas as pd
import requests
KEY = "YOUR_API_KEY"
NAMES = {"T": "ticker", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"}
def one_day(day):
"""Every US stock's daily bar for one date — one request, whatever the row count."""
url = f"https://api.massive.com/v2/aggs/grouped/locale/us/market/stocks/{day}"
r = requests.get(url, params={"adjusted": "false", "apiKey": KEY}, timeout=60)
r.raise_for_status()
rows = r.json().get("results", [])
if not rows: # a weekend or a holiday: no rows, not an error
return None
df = pd.DataFrame(rows).rename(columns=NAMES)
df["date"] = pd.Timestamp(day)
return df[["date", "ticker", "open", "high", "low", "close", "volume"]]
frames = []
for day in pd.bdate_range("2026-09-01", "2026-09-30").strftime("%Y-%m-%d"):
frames.append(one_day(day))
time.sleep(12) # Basic, the free plan, allows five requests a minute
panel = pd.concat([f for f in frames if f is not None], ignore_index=True)
closes = panel.pivot(index="date", columns="ticker", values="close")
adjusted=false is deliberate — the default is split-adjusted, and the section below says why raw
is the thing to store. The endpoint leaves OTC securities out unless you pass include_otc=true.
On the free plan the history window is two years, about 500 trading days, which at five requests
a minute is a backfill of under two hours; from the $29 Starter plan the rate cap goes and the
window is five years.
If the script that sent you here is a yfinance loop failing with YFRateLimitError or a
429 Too Many Requests, passing the whole list to yf.download does not fix it. In yfinance
1.7.0 that call runs Ticker.history once per symbol on a thread pool of up to twice your CPU
cores, so 500 tickers are still 500 requests to Yahoo, several at once. Replacing yfinance in a
script covers the throttling and the port.
What the options are
The approaches above run from one request per date, cheapest entry first, to the whole table in one file, to an honest per-symbol loop.
Massive, the whole US market per date. Covered above. The Massive card has
the plans; history on this endpoint reaches back to 10 September 2003 on the tiers that carry it.
From Starter up there is also a file version: us_stocks_sip/day_aggs_v1 on the S3-compatible
files.massive.com, one gzipped CSV per day, posted by about 11:00 ET the next morning. For a
multi-year backfill the files beat paging the API.
EODHD, one exchange per date, worldwide. EODHD's
eod-bulk-last-day/{exchange} takes a date and returns every instrument on that exchange — US
for the composite, LSE, XETRA and the rest for 60-plus others — at a flat 100 API calls a
request. The vendor's own measurement for 18 August 2026 is 44,376 US rows, 2.3 MB as CSV, and
the rows carry a split-and-dividend adjusted_close beside the raw close. It comes with the $19.99, $29.99 and $99.99
plans, not the free one or the fundamentals-only feed. The arithmetic decides which shape to use: on a 100,000-call daily
allowance, a 3,000-symbol history backfill is 3,000 calls through the per-symbol endpoint, while
20 years of daily files is around 5,000 requests and half a million calls. Backfill per symbol;
keep up to date per day.
day = "2026-10-08"
r = requests.get("https://eodhd.com/api/eod-bulk-last-day/US",
params={"api_token": KEY, "date": day}, timeout=120)
r.raise_for_status()
bars = pd.read_csv(io.StringIO(r.text)) # Code, Ex, Date, Open, High, Low, Close, Adjusted_close, Volume
if not (bars["Date"] == day).all():
raise ValueError(f"asked for {day}, got {bars['Date'].iloc[0]}")
The check on the last two lines is not decoration; the next section says why.
FMP, one date as CSV. Financial Modeling Prep's
/stable/eod-bulk?date= returns open, high, low, close, adjusted close and volume for every symbol
that day as a CSV file. Bulk endpoints are a feature of the Ultimate tier, $149 a month on annual
billing, and FMP meters bandwidth over a trailing 30 days as well as calls — 150 GB at that tier.
EODHD vs Financial Modeling Prep weighs the two.
Databento, the consolidated summary. Databento's EQUS.SUMMARY is
Nasdaq's NLS+ consolidated end-of-day summary for US equities, normalised into the ohlcv-1d
schema.
In the Python client, symbols="ALL_SYMBOLS" asks for the lot in one timeseries.get_range call,
and metadata.get_cost with the same arguments prices it first, which matters on a product billed
by the gigabyte with no free tier beyond the $125 of sign-up credits. Prices are unadjusted;
corporate actions and adjustment factors are separate Databento products. Databento vs
Massive covers where each one fits.
Sharadar, the history as one table. Every endpoint above is date-shaped. Sometimes the job is
the opposite shape — every symbol's whole history once — and Sharadar sells
that: the SEP price table, 21,000 US tickers active and delisted, through a bulk route that
redirects to a zipped CSV at the history length you paid for. Open, high, low, close and volume
are split-adjusted, with closeunadj and closeadj beside them. Prices at full history are $39 a
month on the direct personal licence; commercial use goes through Nasdaq Data Link at an
unpublished price.
Tiingo, a loop that is allowed to finish. Tiingo has no bulk endpoint; daily
prices are /tiingo/daily/{ticker}/prices, one symbol per request. What it has is a budget a loop
can live inside: the $30 Power plan allows 10,000 requests an hour and 100,000 a day, with no
per-minute or per-second cap, so 3,000 symbols' full histories are one run inside an hour. The free
plan stops at 500 unique symbols a month, which a universe of 1,000 meets on day one.
Two things that look like bulk endpoints and are not. Alpha Vantage's
REALTIME_BULK_QUOTES takes up to 100 symbols a request, but it returns quotes, including pre- and
post-market, on a realtime premium plan — a quote is not a daily bar, and there is no bulk daily
history. And Stooq's daily US folder, 515 MB of ASCII across US-listed stocks and
ETFs, is free for personal use and sits behind an image captcha that refused a scripted request on
4 October 2026.
Where this breaks
The date you asked for is not always the date you got. EODHD's bulk endpoint documents two silent fallbacks: a past non-trading day returns the previous trading day — a Sunday gave the Friday, 4 July 2026 gave 2 July — and a future date returns the latest available day. Neither is an error. A nightly job that runs before the data is in, or on a holiday, writes yesterday's rows under today's date and every return for that day comes out as zero. Read the date off the rows, as in the snippet above, and refuse the file when it disagrees. Massive does the honest thing on a holiday and returns no results; treat an empty day as a calendar fact, not a retry.
Adjusted in bulk means adjusted as of when the file was built. Massive's grouped endpoint is split-adjusted by default; its flat files are unadjusted for splits and dividends; Databento's summary is unadjusted; EODHD and FMP carry an adjusted close beside the raw one. An adjusted figure is relative to every event the vendor knew about when it produced the row. Save a year of daily adjusted files and the first split after that makes every stored row before it wrong by the split ratio, while files fetched afterwards are right — a panel assembled day by day ends up on two bases at once. Store raw prices and the splits and dividends beside them, and adjust when you read. Getting split and dividend history as data covers the events; why adjusted close differs covers why two vendors' adjustments disagree even when both are correct.
A file for a date and a history for a symbol are different shapes. The date file is long: one
row per symbol, keyed by whatever the ticker was that day. The symbol history is keyed by today's
ticker. Join them and a renamed company is two tickers in one and one ticker in the other — the
pivot above makes it two columns, each half empty. EODHD's composite file has no venue column
either; every row says US, and the venue lives only in the path you requested (NYSE, NASDAQ,
BATS or AMEX). Map tickers through a security master before joining —
getting a list of every ticker covers renames and identifiers.
"The day" is somebody's day. Databento's documentation says NLS+ publishes its end-of-day
summary three times, at about 16:15, 17:00 and 20:15 ET, that ohlcv-1d carries only the 20:15
one with post-market volume in it, and that this "can lead to discrepancies" against sources using
an earlier one. Massive's flat files are built from activity between 04:00 and 20:00 ET and
stamp it in UTC Unix time; convert to New York time before taking a calendar date from any of it.
Its grouped endpoint's t is a millisecond timestamp that pd.to_datetime(..., unit="ms") turns into
a UTC time, not a date — which is why the snippet sets date from the day it asked for. Before
mixing two vendors' daily bars, compare one ordinary day's close and volume for a few names.
Why two charts show different candles is the
long version.
Delisted names are absent from the shape most loops use. A ticker loop starts from a list, and the list is nearly always today's, so every company that was acquired, went bankrupt or left the index never gets asked for. Date files avoid that by construction for the days they cover — a company is in the file if it traded — but only as far back as the vendor's archive goes, and EODHD's own row counts show coverage growing over time: 2,197 US rows for 2 January 1990, 13,860 for 15 June 2000. Sharadar sells delisted coverage as the point and still calls it 99 per cent. Check one delisting you know before trusting a backtest on the panel; backtesting on survivorship-free data is the rest of that problem.
The limit counts rows, not requests. One bulk request is 100 EODHD calls, and adding
symbols to filter it costs 100 plus one per ticker — measured by the vendor at 102 for two —
so the filtered request costs more than the whole exchange. FMP counts bandwidth over 30 days
beside the call rate, and a wide backfill can exhaust it before the per-minute limit bites.
Databento bills the uncompressed gigabyte. A quota that looked enormous for per-ticker calls is
small for whole markets, and the reverse.
Bulk is the redistribution case. A file with every listed stock's price for every day is the vendor's database, copied, and every self-serve plan on this page is personal or internal use: every EODHD plan below $399 a month is personal use; Massive's individual plans exclude business use; FMP's four listed plans are personal and only its quoted Enterprise plan permits display; Sharadar's personal licence requires deleting every copy within 30 days of cancelling. A panel you built is yours to backtest on, not to publish or share — see redistribution.
If you outgrow this
When the panel has to survive a year of corporate actions, keep it locally as raw bars plus an events table, and re-adjust on read. A columnar file per day and a query engine over the directory is enough at end-of-day sizes; storing minute bars locally covers the same layout one granularity down.
When end-of-day stops being enough, the file-per-day shape carries on into minute bars and trades — Massive's flat files and Databento's batch downloads both go there, and backfilling minute bars is that job.
When the question is one ticker over decades, this page is the wrong tool and downloading historical prices in Python is the right one. The rest of the shelf is in market data APIs, and end-of-day says what the word does and does not promise.
The tools that do this
In the order this page recommends trying them. Paid placement does not affect this order.
Massive
Grouped daily, every US stock's bar for one date per call, on every plan including free Basic at five calls a minute. Split-adjusted unless told not to.
Full-tick US equities, options and futures from a direct exchange feed.
$29/moFree tier
EODHD
eod-bulk-last-day covers a whole exchange, 60-plus of them, for a flat 100 API calls. Not on the free plan, and a holiday quietly returns the day before.
End-of-day and fundamentals for 60+ exchanges worldwide, at a hobbyist price.
$19.99/moFree tier
Financial Modeling Prep
eod-bulk takes one date and returns every symbol as CSV, with adjusted close. Top tier only, and a 30-day bandwidth cap meters the rows.
Financial statements, ratios and filings for 70,000+ securities across 60+ exchanges.
$22/moFree tier
Databento
EQUS.SUMMARY ohlcv-1d with ALL_SYMBOLS, the consolidated Nasdaq NLS+ summary. Unadjusted, billed by the gigabyte, and priced before you send it.
Full order book and tick history from exchange feeds, billed by the gigabyte.
$199/mo
Sharadar
The other shape — the whole US price table, delisted names included, as one zipped CSV. Personal licence; full history is $39 a month.
Point-in-time US fundamentals, EOD prices and 13F tables — now sold direct to individuals.
$9/moFree tier
Tiingo
No bulk endpoint, so a loop, but an honest one — 10,000 requests an hour on the $30 plan, no per-minute cap. Free stops at 500 symbols a month.
End-of-day equity history back to 1962, plus crypto, forex and news, from $30 a month.
$30/moFree tier
FAQ
How do I download prices for all S&P 500 stocks at once?
Ask for the whole market by date and keep the 500 rows you want. One grouped-daily or bulk request returns every US stock's bar for a day, so a year of all 500 is about 250 requests, against 500 history requests in a per-ticker loop. Filter on your list afterwards, and take the list itself for each date in history, not today's, or the panel leaves out every company that left the index.
Does yf.download with a list of tickers make one request?
No. In yfinance 1.7.0, download runs Ticker.history once per symbol on up to twice as many threads as the machine has CPU cores, so 500 tickers are 500 requests to Yahoo, several at a time. A ticker that fails is logged in a "Failed downloads" line and returned as an empty column, not raised as an error.
Is there a free bulk end-of-day download for US stocks?
Massive's grouped daily endpoint is on its free Basic plan, two years back at five requests a minute, which is about 100 minutes for the lot. EODHD and FMP keep their bulk endpoints off the free plan. Stooq's bulk files are free for personal use but sit behind an image captcha, and on 4 October 2026 a scripted request to them was refused.
Should I store adjusted or unadjusted bulk files?
Unadjusted, with splits and dividends stored beside them. An adjusted figure is computed against every event the vendor knew about when it built the row, so a file saved last year does not know about this year's split, and a panel stitched from files saved on different days mixes bases. Raw prices plus the event table can be re-adjusted whenever a new event arrives.
Sources
- Bulk API for EOD, Splits and Dividends — EODHD, read
- Daily Market Summary (OHLC) — Stocks REST API — Massive, read
- Stocks Flat Files — Overview — Massive, read
- Eod Bulk API — Financial Modeling Prep, read
- Databento US Equities Summary — data feed specifications — Databento, read
- Get daily closing prices for equities — Databento, read
- Alpha Vantage API documentation — Realtime Bulk Quotes — Alpha Vantage, read
- yfinance 1.7.0 source — yfinance/multi.py — yfinance project (GitHub),
The catalogue next door
This page names a handful of cards. The rest of them are in Stock Market Data APIs, each filled in against the same schema, with the fields to narrow it yourself.
Last updated . Corrected in place: this is a reference page, not a dated post.