You’re here because something broke. Maybe request #26 of the day came back from Alpha Vantage like this:

Bash
{
    "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_cffi session requirement introduced in yfinance 1.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)yfinanceInfoway API (Free Trial)
Requests25/day, 5/minUndocumented, changes without notice86,400/day, 60/min, 1/sec
AuthAPI keyNone (scraping)API key
Real-time push (WebSocket)NoNoYes
Intraday history depthLimited on free tier1-min: 7 days, 5-min: 60 daysMinute-level: 3 years
ForexYes (FX_DAILYCURRENCY_EXCHANGE_RATE), same 25/day capPoor / unreliableYes, dedicated 85-pair feed
Fundamentals (statements, earnings)Yes, same 25/day capYes, via .info / .financials, unreliable field coverageYes, dedicated endpoints
Market coveragePrimarily US-listedPrimarily US, some international via Yahoo suffixesUS, HK, China A-shares, Japan, Korea, India, forex, crypto, Futures, CFD
SLA / official supportYes (paid tiers)NoneYes

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 yfinance 429s) during normal usage, not just load testing
  • The product has real users and a 429 or a silent empty response is a user-facing outage
  • You need data Alpha Vantage’s free tier or yfinance structurally 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.

Bash
apiKey: YOUR_API_KEY

Python SDK:

Bash
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.USyfinance adds Yahoo-style suffixes for other markets (0700.HK600519.SS).

Infoway does it differently: every symbol, in every market, gets the same {CODE}.{MARKET} suffix.

MarketFormatExample
US Equities{TICKER}.USAAPL.US
Hong Kong{5-digit code}.HK00700.HK
A-Share (China){code}.SH / .SZ600519.SH
Japan{4-digit code}.JP7203.JP
South Korea{6-digit code}.KS005930.KS
India{symbol}.INRELIANCE.IN
ForexCurrency pair, no suffixEURUSD
Bash
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:

Bash
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:

Bash
import yfinance as yf

ticker = yf.Ticker("AAPL")
price = ticker.fast_info["last_price"]

Infoway — one call, up to 100 symbols batched together:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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"])
Bash
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:

Bash
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:

Bash
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: 

Bash
wss://data.infoway.io/ws?business=stock&apikey=YOUR_API_KEY
Bash
import 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

PlanRequests/secondRequests/minuteRequests/dayPrice
Free16086,400Free
Basic2120172,800$99
Premium10600864,000$199
Professional201,2001,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.