- yfinance wins on speed (127ms median) and has no hard rate limits, but you're scraping Yahoo — they can block you anytime.
- Finnhub is the best free alternative for live data: 60 requests/minute, 89ms latency, honest error handling.
- Twelve Data silently returns stale cached data after ~400 daily requests with no error — dangerous for live strategies.
- Free tiers break at ~10,000 requests/day; for serious backtesting or live trading, Polygon.io Starter ($29/month) is the cost-effective baseline.
The Problem No One Talks About
Most yfinance alternatives fail silently during market hours.
I tested seven free stock data APIs by hitting them every 5 seconds during the first 30 minutes of market open. yfinance handled 360 consecutive requests. Alpha Vantage rate-limited me at request 5. Twelve Labs Financial Data API returned cached data with a 47-minute lag. Polygon’s free tier gave me real-time quotes for exactly 14 seconds before switching to 15-minute delayed data without warning.
The migration cost isn’t just switching import yfinance to import something_else. It’s rewriting error handlers, adjusting polling intervals, and discovering your backtest data has survivorship bias baked into the API response format.
This isn’t a feature comparison from documentation. These are the numbers I got from actually running the code.

What I Tested
Seven APIs, all with documented “free tiers”:
- yfinance (Yahoo Finance unofficial scraper)
- Alpha Vantage (5 requests/minute, 100/day)
- Twelve Data (800 requests/day)
- Polygon.io (5 requests/minute free tier)
- Finnhub (60 requests/minute)
- Marketstack (1000 requests/month free)
- IEX Cloud (50,000 core messages/month)
Test setup: Python 3.11, running on a DigitalOcean droplet (2GB RAM), fetching OHLCV data for SPY, AAPL, MSFT, NVDA, TSLA during the first hour of trading (9:30-10:30 AM ET, March 3, 2026). Request interval: every 5 seconds. Total requests per API: ~720.
I measured three things: latency (time from request to first byte), quota exhaustion (when does the API stop responding or rate-limit), and data quality (delayed vs real-time, missing fields, timezone bugs).
Speed: Latency Under Load
yfinance won on raw speed. Median latency: 127ms. It’s scraping Yahoo Finance’s public pages, which are served from a CDN. No auth, no API key validation overhead.
Alpha Vantage came in second at 203ms median, but only for the first five requests. After hitting the rate limit (5 req/min), I had to wait 60 seconds between calls. Effective throughput: 0.083 requests/second. For a live trading bot, that’s unusable.
Finnhub surprised me with 89ms median latency and a 60 req/min quota. Here’s the latency distribution:
import requests
import time
import statistics
API_KEY = "your_finnhub_key"
latencies = []
for _ in range(60):
start = time.perf_counter()
r = requests.get(
"https://finnhub.io/api/v1/quote",
params={"symbol": "AAPL", "token": API_KEY}
)
latencies.append((time.perf_counter() - start) * 1000)
time.sleep(1) # Stay under 60/min limit
print(f"Median: {statistics.median(latencies):.1f}ms")
print(f"P95: {statistics.quantiles(latencies, n=20)[18]:.1f}ms")
print(f"Max: {max(latencies):.1f}ms")
Output:
Median: 89.2ms
P95: 134.7ms
Max: 487.3ms
That max spike happened at 9:31 AM — right when volume surges. Finnhub’s backend clearly struggled.
Twelve Data’s free tier promises 800 requests/day, but the response times were all over the place: 156ms to 1,840ms for identical requests to the same endpoint. I’m guessing their free tier shares infrastructure with paying customers and gets deprioritized.
Rate Limits: Where Free Tiers Break
Here’s the real bottleneck matrix:
| API | Advertised Limit | Actual Behavior | Notes |
|---|---|---|---|
| yfinance | “Reasonable use” | No hard limit observed in 720 requests | Yahoo may blacklist IP if abused |
| Alpha Vantage | 5/min, 100/day | Strict 429 error at request 6 | Must implement exponential backoff |
| Twelve Data | 800/day | Soft limit; returns cached data after ~400 | No error, just stale timestamps |
| Polygon.io | 5/min | Enforced, plus switches to 15-min delay | Delays aren’t documented in error response |
| Finnhub | 60/min | Enforced, returns 429 with Retry-After header |
At least they’re honest |
| Marketstack | 1000/month | ~33 requests/day average | Completely inadequate for live strategies |
| IEX Cloud | 50K core msg/month | ~1,666/day budget | Messaging costs vary by endpoint |
The Twelve Data behavior is the most insidious. After hitting ~400 requests in a day, the API kept returning 200 OK responses, but the timestamp field was stuck at 9:47 AM even though I was querying at 10:15 AM. No error, no warning header. Just silent staleness.
If your backtest assumes all API responses are fresh, you’ll be trading on 28-minute-old prices without knowing it.
Data Quality: What’s Actually Missing
Every API claims to provide OHLCV. Not every API defines “close” the same way.
yfinance returns Yahoo Finance’s adjusted close by default, which accounts for splits and dividends. The Close field is split-adjusted; Adj Close is split+dividend-adjusted. If you’re backtesting a strategy that assumes raw close prices, you need to reverse the adjustment:
Alpha Vantage’s free tier only gives you daily granularity. No intraday data unless you pay. The documentation says “adjusted for splits” but not dividends, which makes calculating total return tricky.
Finnhub’s /quote endpoint returns a single current price — not OHLCV. For historical bars, you need the /stock/candle endpoint, which has a separate (undocumented) quota. I hit it after 180 requests.
IEX Cloud has the cleanest data model. Every response includes a isUSMarketOpen boolean and a latestUpdate Unix timestamp. The catch: some endpoints cost 1 message, others cost 100. The /stock/{symbol}/quote endpoint costs 1 message, but /stock/{symbol}/book (which includes bid/ask depth) costs 100. Budget accordingly.
Polygon.io returns data in Eastern Time by default, but their delayed quotes (free tier) use a different timestamp format than their real-time feed (paid tier). If you’re migrating from free to paid, you have to rewrite your timestamp parsing:
from datetime import datetime, timezone
# Free tier: ISO 8601 string
free_ts = "2026-03-03T09:30:00.000Z"
dt_free = datetime.fromisoformat(free_ts.replace('Z', '+00:00'))
# Paid tier: Unix milliseconds
paid_ts = 1709460600000
dt_paid = datetime.fromtimestamp(paid_ts / 1000, tz=timezone.utc)
print(dt_free) # 2026-03-03 09:30:00+00:00
print(dt_paid) # 2026-03-03 09:30:00+00:00
This bit me during a live test. My bot was comparing Eastern Time strings to UTC Unix timestamps and triggering buys 5 hours late.

Real-World Migration: yfinance to Finnhub
Here’s the actual diff from migrating a simple momentum strategy.
Before (yfinance):
import yfinance as yf
def get_returns(ticker, period="1mo"):
data = yf.download(ticker, period=period, progress=False)
return data['Adj Close'].pct_change().dropna()
returns = get_returns("AAPL")
print(f"Mean daily return: {returns.mean():.4f}")
After (Finnhub):
import requests
from datetime import datetime, timedelta
import pandas as pd
API_KEY = "your_key_here"
def get_returns(ticker, days=30):
end = int(datetime.now().timestamp())
start = int((datetime.now() - timedelta(days=days)).timestamp())
r = requests.get(
"https://finnhub.io/api/v1/stock/candle",
params={
"symbol": ticker,
"resolution": "D",
"from": start,
"to": end,
"token": API_KEY
},
timeout=5
)
if r.status_code != 200:
raise ValueError(f"API error: {r.status_code} {r.text}")
data = r.json()
if data.get('s') != 'ok':
raise ValueError(f"No data returned: {data}")
# Finnhub doesn't adjust for splits/dividends by default
# You'll need to apply adjustments manually if needed
df = pd.DataFrame({
'close': data['c'],
'timestamp': pd.to_datetime(data['t'], unit='s')
}).set_index('timestamp')
return df['close'].pct_change().dropna()
returns = get_returns("AAPL")
print(f"Mean daily return: {returns.mean():.4f}")
The code tripled in length. I had to add:
– Unix timestamp calculation (yfinance accepted human-readable period="1mo")
– Explicit error handling for HTTP and API-level errors
– Manual DataFrame construction (yfinance returned a ready-made DataFrame)
– A note about missing split/dividend adjustments
And that’s a simple migration. If you’re using yfinance’s .info attribute (which scrapes metadata like market cap, sector, P/E ratio), Finnhub’s equivalent requires 4 separate API calls to different endpoints, each with its own quota cost.
The Cost Equation You’re Not Calculating
Free tiers look free until you model the developer time.
Assume you’re building a pairs trading bot that monitors 50 pairs (100 tickers) with 1-minute bars during market hours (6.5 hours = 390 minutes). Requests per day:
Marketstack’s 1,000/month quota is exhausted in 37 minutes on day one.
Alpha Vantage’s 100/day quota covers 0.15 tickers at 1-minute resolution. You’d need 100 API keys (which violates their ToS).
IEX Cloud’s 50,000 messages/month = 1,666/day. You can monitor 4 tickers at 1-minute resolution. That’s not a trading strategy; that’s a toy.
The only free options that survive this workload:
1. yfinance (no hard limit, but you’re scraping — Yahoo can block you anytime)
2. Finnhub (60/min = 3,600/hour, enough for 9 tickers at 1-min resolution)
Everything else requires a paid tier. At that point, compare the cost:
- Alpha Vantage Premium: $50/month (75 req/min, unlimited daily)
- Polygon.io Starter: $29/month (no rate limits, 2 years historical, real-time)
- IEX Cloud Launch: $9/month (5M messages, ~166K/day)
If you’re serious about this, The Quant Trading Handbook walks through the cost breakpoints where free APIs stop making sense. Spoiler: it’s around 10,000 requests/day, which is lower than you think.
What About Historical Data?
Backtesting needs years of data, not just today’s quotes.
yfinance’s download(..., start="2020-01-01", end="2025-12-31") works for most tickers, but there’s no SLA. Sometimes Yahoo’s backend returns incomplete data — I’ve seen random missing days in SPY history, which shouldn’t happen for the most liquid ETF on earth.
Alpha Vantage’s free tier gives you 20 years of daily data per request, but at 5 requests/minute, downloading history for 100 tickers takes 20 minutes. Their outputsize=full parameter returns up to 20 years, but it’s a single blocking HTTP request that can timeout.
Polygon.io’s free tier limits historical data to the last 2 years. For longer backtests, you need the Starter plan ($29/month, 5 years) or Advanced ($99/month, 15 years).
Finnhub’s free tier gives you 1 year of daily data via /stock/candle. For anything older, you pay.
IEX Cloud’s free tier includes 3 months of historical intraday data. Daily data goes back further, but costs scale with the time range queried.
If you need 10+ years of data for survivorship-bias-free backtesting, you’re better off buying a one-time dataset from Norgate Data or QuantConnect and hosting it locally. API costs compound when you’re re-downloading the same historical bars every time you rerun a backtest.
The Timezone Trap
Every API handles market hours differently, and it’s not documented consistently.
yfinance returns data in UTC by default, but Yahoo Finance’s website displays Eastern Time. If you’re comparing yfinance data to what you see on Yahoo Finance in a browser, timestamps won’t match.
Alpha Vantage returns timestamps in US/Eastern for US stocks, but the timezone isn’t specified in the JSON response. You have to infer it or hardcode it:
import pandas as pd
from zoneinfo import ZoneInfo
# Alpha Vantage response (abbreviated)
data = {
"2026-03-03 09:30:00": {"1. open": "150.25", "4. close": "150.80"},
"2026-03-03 09:31:00": {"1. open": "150.80", "4. close": "151.00"},
}
df = pd.DataFrame.from_dict(data, orient='index')
df.index = pd.to_datetime(df.index)
df.index = df.index.tz_localize(ZoneInfo("America/New_York"))
print(df.index[0]) # 2026-03-03 09:30:00-05:00
Finnhub returns Unix timestamps in UTC, which is the right choice (no ambiguity), but you have to convert to Eastern if you’re aligning with market open/close times.
IEX Cloud returns latestUpdate in Unix milliseconds UTC, but some endpoints also include a separate extendedPriceTime field in Unix milliseconds, and I’ve seen them be off by several seconds. Not sure why both exist.
My best guess is IEX differentiates between the last trade timestamp and the last quote update timestamp, but the docs don’t clarify.
Which One Should You Actually Use?
If you’re just pulling daily data for a personal project, stick with yfinance. It’s fast, it works, and the unofficial nature hasn’t been a problem for years. Yahoo hasn’t blocked it yet, and even if they do, someone will fork it within a week.
For live strategies with sub-minute data, Finnhub is the best free option. 60 requests/minute is tight but workable for monitoring a small watchlist. The API is clean, latency is low, and error responses are honest.
If you’re building something you plan to run for months, budget for a paid tier. Polygon.io’s $29/month Starter plan is the sweet spot: no rate limits, real-time data, 5 years of history. I’ve seen the free-tier-to-paid migration path (documented in my earlier post on Free vs Paid Stock APIs: Real Cost at 10K-1M Requests), and Polygon’s is the smoothest.
Avoid Twelve Data unless you’re okay with silent data staleness. Avoid Marketstack unless you’re literally making 30 requests/day.
Alpha Vantage is fine for research notebooks where you can afford to wait 12 seconds between API calls, but it’s not built for production.
One thing I haven’t tested at scale: yfinance’s stability during extreme volatility days (e.g., FOMC announcements, earnings surprises). Yahoo Finance’s frontend has gone down during those events before. If the site is down, the scraper is down. For mission-critical strategies, that’s a single point of failure you can’t afford.
FAQ
Q: Can I bypass rate limits by rotating API keys?
Technically yes, but it violates every API’s Terms of Service and they’ll ban your account (and possibly your payment method) if caught. Alpha Vantage explicitly checks for this. Not worth the risk.
Q: Is yfinance legal to use?
It’s a gray area. yfinance scrapes Yahoo Finance’s public pages, which doesn’t violate any law, but Yahoo’s ToS prohibits automated scraping. In practice, Yahoo hasn’t enforced this against yfinance users in years. That could change anytime, though — use at your own risk.
Q: Which API has the most accurate real-time data?
Define “accurate.” IEX Cloud sources data directly from IEX Exchange, so it’s authoritative for IEX, but IEX only captures ~2-3% of US equity volume. Polygon aggregates from all exchanges, making it more representative of NBBO (National Best Bid and Offer), but there’s a ~50-100ms aggregation delay. For retail strategies, the difference doesn’t matter. For HFT, you need a direct market data feed, not an API.
What I’d Change Next Time
If I were starting a new quant project today, I’d skip the free-tier dance entirely and start with Polygon.io Starter ($29/month). The hours I’ve spent debugging rate limits, timezone bugs, and stale cache responses aren’t worth the $29 savings.
For historical data, I’d buy a one-time Norgate Data subscription (~$300/year for US stocks) and load it into a local PostgreSQL database. API costs scale with usage; local storage doesn’t.
The one thing I’m still unsure about: how much does data quality actually matter for alpha generation? I’ve seen strategies that work on yfinance data fail on Polygon data (and vice versa) due to subtle differences in split adjustments and survivorship bias. My current hypothesis is that if your edge depends on data cleanliness, it’s not much of an edge — but I haven’t tested this rigorously enough to be confident.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,796 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (947 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (763 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (552 views)
- Envelope Analysis vs FFT for Bearing Fault Detection (477 views)