You’re here because something broke. Maybe request #26 of the day came back from Alpha Vantage like this:
{
"Information": "Thank you for using Alpha Vantage! Our standard API rate limit is 25 requests per day..."
}
Or maybe it was yfinance — a YFRateLimitError, or worse, no error at all, just an empty DataFrame where your price data should be.
Alpha Vantage and Yahoo Finance are genuinely good at what they’re built for. A prototype. A weekend project. A notebook you run a couple of times a week. Neither was built to sit behind something with real users hitting it on a schedule.
This guide is for the point where you’ve decided to move off one of them, or both. It covers three things: exactly what limit you’re hitting and why it exists, a quick way to check whether migrating is actually worth it yet, and a field-by-field mapping from Alpha Vantage and yfinance calls to their Infoway API equivalents, with working code for each one.
The Two Free Tiers You’re Choosing Between
Alpha Vantage is a real API, an API key, documented endpoints, a support team behind it. But the free tier has been cut twice since launch. It started at 500 requests/day. Then 100/day. As of 2026, it’s 25 requests/day, capped at 5 requests/minute.
That’s not a lot. A dashboard refreshing a handful of tickers on page load can burn through 25 requests before lunch. And some functions, like real-time intraday quotes, or the full intraday history data, sit behind the paid tiers entirely. So even the calls you do get for free often come back 15-minute-delayed or truncated.
yfinance is a different kind of limited. It isn’t an API in the normal sense, it’s an open-source library that scrapes Yahoo Finance’s internal, unofficial endpoints. A few consequences follow from that:
- No API key, no published rate limit, no SLA. Yahoo can throttle, block, or restructure the response at any time, without notice.
- Heavy or parallel requests reliably trigger
429s or silent empty DataFrames, especially around market open and earnings releases. - Breaking changes land with no changelog. The
curl_cffisession requirement introduced inyfinance1.x is a recent example: a fix on Yahoo’s side that broke existing integrations overnight.
Neither problem is one you can code your way around. Alpha Vantage’s 25/day cap is a business decision, not a technical ceiling — better retry logic won’t raise it. yfinance‘s instability is structural: you’re depending on a website’s internal API staying stable for a use case it was never built to support.
What “Free” Actually Gets You, Side by Side
| Alpha Vantage (Free) | yfinance | Infoway API (Free Trial) | |
|---|---|---|---|
| Requests | 25/day, 5/min | Undocumented, changes without notice | 86,400/day, 60/min, 1/sec |
| Auth | API key | None (scraping) | API key |
| Real-time push (WebSocket) | No | No | Yes |
| Intraday history depth | Limited on free tier | 1-min: 7 days, 5-min: 60 days | Minute-level: 3 years |
| Forex | Yes (FX_DAILY, CURRENCY_EXCHANGE_RATE), same 25/day cap | Poor / unreliable | Yes, dedicated 85-pair feed |
| Fundamentals (statements, earnings) | Yes, same 25/day cap | Yes, via .info / .financials, unreliable field coverage | Yes, dedicated endpoints |
| Market coverage | Primarily US-listed | Primarily US, some international via Yahoo suffixes | US, HK, China A-shares, Japan, Korea, India, forex, crypto, Futures, CFD |
| SLA / official support | Yes (paid tiers) | None | Yes |
Look at both columns on the left and a pattern shows up: whatever you’d actually want most from either tool — more requests, real-time updates, deeper history — is exactly what’s rationed hardest.
Should You Actually Migrate Yet?
Not every yfinance or Alpha Vantage user needs to move today. Migrate when at least one of these is true:
- You’re regularly hitting the rate limit (Alpha Vantage’s 25/day, or repeated
yfinance429s) during normal usage, not just load testing - The product has real users and a
429or a silent empty response is a user-facing outage - You need data Alpha Vantage’s free tier or
yfinancestructurally can’t give you, real-time push, deep intraday history, non-US markets, level-2 depth - You’ve caught yourself writing retry/backoff/caching code whose only purpose is working around the rate limit rather than solving a product problem
Still prototyping, still comfortably inside 25 requests/day?
No urgency. This guide will still be here when you outgrow it.
Getting Set Up
Register at infoway.io for an API key. The free trial needs no credit card, and it runs at the rate limits in the table above, plenty to validate a migration before touching production traffic.
apiKey: YOUR_API_KEY
pip install infoway-sdk
Every example below shows the raw requests call too, usually the fastest way to confirm field mappings line up while you’re migrating.
Symbol Format
Alpha Vantage and yfinance both use bare tickers for US equities: AAPL, not AAPL.US. yfinance adds Yahoo-style suffixes for other markets (0700.HK, 600519.SS).
Infoway does it differently: every symbol, in every market, gets the same {CODE}.{MARKET} suffix.
| Market | Format | Example |
|---|---|---|
| US Equities | {TICKER}.US | AAPL.US |
| Hong Kong | {5-digit code}.HK | 00700.HK |
| A-Share (China) | {code}.SH / .SZ | 600519.SH |
| Japan | {4-digit code}.JP | 7203.JP |
| South Korea | {6-digit code}.KS | 005930.KS |
| India | {symbol}.IN | RELIANCE.IN |
| Forex | Currency pair, no suffix | EURUSD |
def to_infoway_symbol(ticker: str, market: str = "US") -> str:
"""Convert a bare ticker (Alpha Vantage / yfinance US style) to Infoway's suffixed format."""
return f"{ticker.upper()}.{market}"
to_infoway_symbol("AAPL") <em># "AAPL.US"</em>
One quiet trap to watch for: yfinance uses .SS for Shanghai-listed A-shares, Infoway uses .SH. Bulk-migrating a symbol list from yfinance? That’s a find-and-replace, not a straight copy.
Scenario 1: Latest Quote
Alpha Vantage — GLOBAL_QUOTE, one symbol per call, counted against your 25/day:
import requests
resp = requests.get("https://www.alphavantage.co/query", params={
"function": "GLOBAL_QUOTE",
"symbol": "AAPL",
"apikey": "YOUR_AV_KEY",
})
quote = resp.json()["Global Quote"]
price = quote["05. price"]
yfinance — .info or .fast_info, one HTTP round trip to Yahoo per ticker:
import yfinance as yf
ticker = yf.Ticker("AAPL")
price = ticker.fast_info["last_price"]
Infoway — one call, up to 100 symbols batched together:
from infoway import InfowayClient
client = InfowayClient(api_key="YOUR_API_KEY")
trades = client.stock.get_trade("AAPL.US,TSLA.US,MSFT.US")
for t in trades:
print(f"{t['s']:10s} ${t['p']:>10s} vol={t['v']:>10s}")
Batching is the detail that matters most here. If your old code called Alpha Vantage or yfinance once per symbol in a loop, that pattern alone burns through a 25/day cap with just five tickers, or trips yfinance‘s undocumented parallel-request throttle.
Collapsing that loop into one batched Infoway call is usually the single biggest drop in request volume you’ll see from this migration.
Scenario 2: Historical Prices (Daily and Intraday)
Alpha Vantage — TIME_SERIES_DAILY / TIME_SERIES_INTRADAY, returned as a date-keyed dict:
resp = requests.get("https://www.alphavantage.co/query", params={
"function": "TIME_SERIES_DAILY",
"symbol": "AAPL",
"outputsize": "full",
"apikey": "YOUR_AV_KEY",
})
series = resp.json()["Time Series (Daily)"]
for date, bar in series.items():
close = bar["4. close"]
yfinance — .history(), returned as a pandas DataFrame:
df = yf.download("AAPL", period="1y", interval="1d")
Infoway — the candlestick (OHLCV) endpoint, with 12 timeframes instead of Alpha Vantage’s fixed daily/weekly/monthly functions or yfinance‘s interval strings:
from infoway import InfowayClient, KlineType
client = InfowayClient(api_key="YOUR_API_KEY")
df = client.stock.get_kline("AAPL.US", kline_type=KlineType.DAY, count=250)
print(df.tail())
Full history with pagination (the equivalent of Alpha Vantage’s outputsize=full or yfinance‘s period="max") — a single request maxes out at 500 candles, so walk backward using timestamp:
import requests, json
API_KEY = "YOUR_API_KEY"
ENDPOINT = "https://data.infoway.io/stock/v2/batch_kline"
HEADERS = {"Content-Type": "application/json", "apiKey": API_KEY}
def download_full_history(symbol: str, kline_type: int = 8) -> list[dict]:
all_candles, cursor = [], None
while True:
payload = {"codes": symbol, "klineType": kline_type, "klineNum": 500}
if cursor:
payload["timestamp"] = cursor
resp = requests.post(ENDPOINT, headers=HEADERS, data=json.dumps(payload), timeout=30)
resp.raise_for_status()
items = resp.json().get("data", [])
candles = next((it["respList"] for it in items if it["s"] == symbol), [])
if not candles:
break
all_candles.extend(candles)
oldest_ts = int(candles[-1]["t"])
if cursor and oldest_ts >= cursor:
break
cursor = oldest_ts
all_candles.sort(key=lambda c: int(c["t"]))
return all_candles
candles = download_full_history("AAPL.US")
print(f"AAPL.US: {len(candles)} daily bars")
Intraday is where the free tiers hurt most.
Alpha Vantage caps 1-minute history on the free plan, and yfinance limits 1-minute bars to the trailing 7 days. Infoway’s minute-level data goes back 3 years. If “I need more than a week of 1-minute bars for backtesting” is the actual reason you’re reading this, that gap alone is worth the migration.
Scenario 3: Fundamentals
Alpha Vantage — OVERVIEW for key stats, EARNINGS for the beat/miss history, each a separate call against the same 25/day budget:
overview = requests.get("https://www.alphavantage.co/query", params={
"function": "OVERVIEW", "symbol": "AAPL", "apikey": "YOUR_AV_KEY",
}).json()
pe_ratio = overview["PERatio"]
market_cap = overview["MarketCapitalization"]
earnings = requests.get("https://www.alphavantage.co/query", params={
"function": "EARNINGS", "symbol": "AAPL", "apikey": "YOUR_AV_KEY",
}).json()["quarterlyEarnings"]
yfinance — .info for the same key stats (field coverage varies by ticker and changes without notice), .financials / .balance_sheet / .cashflow for statements:
info = yf.Ticker("AAPL").info
pe_ratio, market_cap = info.get("trailingPE"), info.get("marketCap")
income_stmt = yf.Ticker("AAPL").financials
Infoway — split into purpose-built endpoints rather than one blended object:
from infoway import InfowayClient
client = InfowayClient(api_key="YOUR_API_KEY")
company = client.stock_info.get_company("AAPL.US")
print(company["company_name"], "-", company["industry"])
import requests
headers = {"apiKey": "YOUR_API_KEY"}
info = requests.get(
"https://data.infoway.io/common/basic/symbols/info",
headers=headers,
params={"type": "STOCK_US", "symbols": "AAPL.US"},
).json()["data"][0]
valuation = requests.get(
"https://data.infoway.io/common/v2/basic/stock/valuation/AAPL.US",
headers=headers,
).json()["data"]
latest_pe = valuation["pe_list"][-1]["pe"]
earnings = requests.get(
"https://data.infoway.io/common/basic/financial/earnings",
headers=headers,
params={"symbol": "AAPL.US", "type": "STOCK_US", "period_type": "fq"},
).json()["data"]
for e in earnings:
beat = "beat" if e["epsPercentage"] > 0 else "miss"
print(f"{e['periodKey']}: EPS {e['epsActual']} vs est {e['epsEstimate']} ({beat})")
income_stmt = requests.get(
"https://data.infoway.io/common/basic/financial/income_statement",
headers=headers,
params={"symbol": "AAPL.US", "type": "STOCK_US", "period_type": "fq"},
).json()["data"]
One difference worth noting: the valuation endpoint returns a full PE/PB time series, not a single snapshot. If your old code was polling OVERVIEW daily just to build that history yourself, you can delete that job.
Scenario 4: Forex
yfinance forex support is thin and unreliable, so most developers looking for free forex data end up on Alpha Vantage’s FX_DAILY or CURRENCY_EXCHANGE_RATE instead, still capped at the same 25 requests/day as every other function on the account.
Alpha Vantage:
fx = requests.get("https://www.alphavantage.co/query", params={
"function": "CURRENCY_EXCHANGE_RATE",
"from_currency": "EUR", "to_currency": "USD",
"apikey": "YOUR_AV_KEY",
}).json()["Realtime Currency Exchange Rate"]
rate = fx["5. Exchange Rate"]
Infoway — forex uses the same /common/ endpoints as commodities and precious metals, no separate function per data type:
import requests
headers = {"apiKey": "YOUR_API_KEY"}
resp = requests.get(
"https://data.infoway.io/common/batch_trade/EURUSD,GBPUSD,USDJPY",
headers=headers,
).json()["data"]
for tick in resp:
print(f"{tick['s']}: {tick['p']}")
That’s the difference in a sentence: Infoway’s forex feed covers 85 pairs with sub-100ms latency over WebSocket, versus a CURRENCY_EXCHANGE_RATE call you’re rationing to a handful of times a day.
Scenario 5: WebSocket
Neither Alpha Vantage nor yfinance offers a push connection. Both are request/response only. So “real-time” in your old code really meant polling on a timer and hoping you didn’t hit the rate limit before the next tick. Infoway’s WebSocket removes that loop entirely.
Endpoint:
wss://data.infoway.io/ws?business=stock&apikey=YOUR_API_KEYimport asyncio, json, uuid, logging
import websockets
from websockets.exceptions import ConnectionClosed
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
logger = logging.getLogger("stock-stream")
SYMBOLS = "AAPL.US,TSLA.US,MSFT.US"
class StockStreamClient:
def __init__(self, api_key: str):
self.url = f"wss://data.infoway.io/ws?business=stock&apikey={api_key}"
self.ws = None
self.running = True
async def _send(self, msg: dict):
await self.ws.send(json.dumps(msg))
async def _subscribe(self):
trace = lambda: str(uuid.uuid4())
await self._send({"code": 10000, "trace": trace(), "data": {"codes": SYMBOLS}}) <em># trades</em>
await asyncio.sleep(1)
await self._send({"code": 10006, "trace": trace(),
"data": {"arr": [{"type": 1, "codes": SYMBOLS}]}}) <em># 1-min candles</em>
async def _heartbeat(self):
while True:
await asyncio.sleep(30)
if self.ws is None:
break
await self._send({"code": 10010, "trace": str(uuid.uuid4())})
def _dispatch(self, raw: str):
msg = json.loads(raw)
code, data = msg.get("code"), msg.get("data", {})
if code == 10002:
logger.info("TRADE %-10s $%-10s vol=%s", data.get("s"), data.get("p"), data.get("v"))
elif code == 10008:
logger.info("CANDLE %-10s c=%s vol=%s", data.get("s"), data.get("c"), data.get("v"))
async def start(self):
backoff = 5
while self.running:
try:
async with websockets.connect(self.url) as ws:
self.ws = ws
await self._subscribe()
hb = asyncio.create_task(self._heartbeat())
async for message in ws:
self._dispatch(message)
hb.cancel()
backoff = 5
except ConnectionClosed as e:
logger.warning("Connection closed: %s — reconnecting in %ss", e, backoff)
except Exception as e:
logger.error("Error: %s", e)
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 60)
async def main():
client = StockStreamClient(api_key="YOUR_API_KEY")
await client.start()
if __name__ == "__main__":
asyncio.run(main())
Rate Limits by Plan
| Plan | Requests/second | Requests/minute | Requests/day | Price |
|---|---|---|---|---|
| Free | 1 | 60 | 86,400 | Free |
| Basic | 2 | 120 | 172,800 | $99 |
| Premium | 10 | 600 | 864,000 | $199 |
| Professional | 20 | 1,200 | 1,728,000 | $399 |
Even the Free tier alone (86,400 requests/day) is over 3,000x Alpha Vantage’s free cap. That’s enough headroom to migrate, test, and run a small production workload before a paid plan even enters the conversation.
FAQ
Is Alpha Vantage’s 25-requests-a-day limit ever going to change?
It’s already been cut twice. 500/day, then 100/day, then 25/day, each time with little advance notice to existing free users. Nothing suggests it’s going back up. If you’re planning around “it’ll probably stay near this level,” that assumption has already failed twice.
Can I just pay for Alpha Vantage Premium instead of migrating?
Sure, that’s a valid option. The paid tiers raise the request cap and unlock full intraday history, and if Alpha Vantage’s function set already fits your code well, staying and upgrading is reasonable. This guide is for a different case: you also want real-time data via WebSocket, coverage beyond US equities, or you just don’t want a scraper with no SLA (yfinance) anywhere in the stack.
Does yfinance have a paid tier I could upgrade to instead?
No. It isn’t a product, it’s a wrapper around Yahoo Finance’s internal, unofficial endpoints. No support contract, no rate-limit upgrade, no guarantee Yahoo won’t change the response tomorrow. An SLA only comes from moving to an API that actually offers one.
Do I need to rewrite every symbol reference in my codebase?
Only where the request gets built. Appending .US, or converting a yfinance-style .SS/.HK suffix, is a one-line change, see the helper function above. If symbols live in a database, migrate the stored values once, or wrap the conversion at your data-access layer and leave the rest of the codebase alone.
Is there a free way to test the migration before switching production traffic?
Yes. The Infoway free trial needs no credit card and runs at the Free-tier limits in the table above — plenty to confirm your endpoint mapping and parsing logic match your old Alpha Vantage or yfinance output before you cut over.
My code only pulls US stocks, is there any reason to migrate beyond the rate limit?
If Alpha Vantage’s 25/day or yfinance‘s instability genuinely isn’t hurting you, the rate limit alone isn’t a reason to move. The reasons that actually come up: wanting WebSocket push instead of polling, needing more than 7 days of 1-minute history, or wanting an SLA-backed vendor instead of an unofficial scraper as the thing your product depends on.