Can CeylanVienna-based, globally curious.
Learn/Backend

Financial data APIs fail silently — design for None, not for errors

Rate-limited financial data APIs don't raise exceptions. They return empty DataFrames and None values while your logs show no errors. Every metric computation must handle this explicitly.

2026-04-18·3 min read·beginner

The surprising failure mode

When you make too many requests to a free financial data API too quickly, you expect an error. An exception, a 429 status, something your try/except can catch.

What you get instead is silence. The response arrives normally. The data structure is there. But the values inside are None, NaN, or an empty DataFrame.

Your application continues. No exception is raised. No error is logged. Your dashboard shows dashes instead of numbers, and if you're not looking carefully, you might not notice for hours.

Why this happens

Free-tier financial data APIs are built to be graceful. Raising exceptions on rate limits would break the many scripts that don't handle errors properly. Returning empty data is safer — callers that check for data presence handle it correctly; callers that don't were probably going to crash anyway.

The API is also incentivised to give you something. A response that looks valid but contains no useful data is harder to detect and easier to attribute to "the company hasn't reported yet" than a hard error.

The pattern: accept None at every layer

The correct design accepts None explicitly at every step of the data pipeline:

def get_metric(ticker: str, metric: str) -> float | None:
    try:
        info = fetch_ticker_info(ticker)
        value = info.get(metric)
        if value is None or (isinstance(value, float) and math.isnan(value)):
            return None
        return float(value)
    except Exception:
        return None

def compute_score(roic: float | None, margin: float | None) -> int | None:
    if roic is None or margin is None:
        return None  # can't score without data
    ...

Every function that touches external data returns Optional. Every function that computes on that data checks for None before computing. Never assume the data arrived.

The rate limiting discipline

Beyond handling None, you need to prevent the condition in the first place:

TICKERS = ["AAPL", "MSFT", "GOOGL", ...]

for ticker in TICKERS:
    data = fetch_ticker(ticker)
    process(data)
    time.sleep(2)  # mandatory between requests

Two seconds between requests is a reasonable floor for free-tier APIs. Reduce it and you'll see more None returns. The sleep is not optional.

Do not use asyncio.gather() or parallel threads for financial data fetching from free APIs. The throughput gain is not worth the data quality loss.

Making the absence visible

The right UX for missing data is explicit absence, not zero:

  • Show not 0 when a metric couldn't be fetched
  • Show N/A not a calculated score when inputs are missing
  • Log a warning (not an error) when a ticker returns empty data

Zero and are different claims. Zero means the metric was measured and is zero. Dash means the metric wasn't available. Confusing them produces false analysis.

Layering data sources

For cases where data quality is critical, fetch from multiple sources and use the first non-None value:

def get_cash_flow(ticker: str) -> float | None:
    # Try primary source first
    primary = fetch_from_primary(ticker)
    if primary is not None:
        return primary
    
    # Fall back to secondary source (e.g. SEC filings)
    return fetch_from_sec(ticker)

This layering pattern gives you resilience against any single source's rate limits or data gaps, without requiring both sources to be available simultaneously.

More like this, straight to your inbox.

I write about Backend and a handful of other things I actually care about. No schedule, no filler — just when I have something worth saying.

More on Backend

Separate the editorial date from the publish timestamp — they mean different things

Content systems routinely conflate two different concepts: the date the author wrote something, and when it was actually published. Treating them as one field causes sorting bugs, broken date displays, and incorrect analytics. They need to be separate from the start.

Cache AI API results by content hash to prevent cost explosions

Users upload the same image multiple times. AI APIs charge per call. A cache keyed on SHA-256 of the input bytes ensures you pay for each unique input once — not once per upload.

Scheduled publishing without a cron: runtime-evaluated date filters

You don't need a cron job to make content appear on schedule. Evaluate the scheduled date at request time and the content becomes visible automatically — no deployment, no job, no database update required.

If this raised a question, I'd be happy to talk about it.

Find me →
← Back to Learn