D
P
0

Next.js

Fear & Greed Index Stuck at 72 While the API Says 26: a Hardcoded Placeholder Hiding an Outage

July 16, 2026·4 min read
Fear & Greed Index Stuck at 72 While the API Says 26: a Hardcoded Placeholder Hiding an Outage

The client sent me a single screenshot. The Fear & Greed Index widget on a crypto news site I was building for him showed 72, labeled "Greed", needle sitting comfortably in the greedy zone. Below it was a second screenshot, straight from alternative.me: 26, "Fear". That is not a rounding difference. One number says the market is greedy, the other says the market is scared. And the wrong number had been live for hours, possibly days, with nobody noticing. No error on the page, no alert, no red line in any log. The site looked perfectly healthy. That was the worst part.

The misleading trail

My first move was the standard one: verify upstream. I hit the endpoint directly from the terminal:

curl "https://api.alternative.me/fng/?limit=1"

The API was alive and answered 26. So the data was correct upstream and wrong downstream. Suspicion number two: the cache. The widget reads from Upstash Redis with a one hour TTL, so a slightly stale number would be normal. But one hour of staleness does not explain 72 versus 26 persisting far longer than that. A stale cache can only serve an old value that was once true. This 72 was not a value that had been true any time recently.

That is when the number itself started to feel off: 72 was too tidy. Not NaN, not 0, not an empty widget. A plausible value with a matching classification label. When a value is wrong but looks convincing, my suspicion moves from the data to the code.

Root cause: a fallback that invents numbers

I opened lib/api/fear-greed.ts and found it within seconds:

const PLACEHOLDER: FearGreedIndex = {
  value: 72,
  classification: "Greed",
  // ...remaining fields filled in loosely
};

The fetch function returned this PLACEHOLDER on any failure at all, including a stale cache. API timeout? 72. Malformed response? 72. alternative.me fully down? Still 72, labeled "Greed", rendered with total confidence as if it had been fetched seconds ago.

The original intent was probably noble: never leave the widget empty. In practice it meant that every time alternative.me had a problem, the site displayed a plausible fictional number. And because that result also landed in the cache with a one hour TTL, the fiction outlived the upstream incident that caused it.

This is the class of bug I try hardest to avoid: not the kind that crashes, the kind that fails silently. The error gets swallowed cleanly, monitoring sees nothing because technically nothing failed, and the only detection system left is a client who happens to compare his own site against the original source.

The fix: be honest about failure

The fix has three layers. First, the function contract changed. fetchFearGreedIndex() now returns FearGreedIndex | null, and null means it failed, full stop:

export async function fetchFearGreedIndex(): Promise<FearGreedIndex | null> {
  const res = await fetch("https://api.alternative.me/fng/?limit=1")
    .catch(() => null);
  if (!res || !res.ok) return null; // a failure is a failure, do not invent
  // parse and validate as usual
}

Second, the UI stopped pretending. When the data is null, it renders an explicit message instead of a made-up number:

{fng ? (
  <FearGreedGauge value={fng.value} bare />
) : (
  <div>Index temporarily unavailable.</div>
)}

Third, the cache layer got cleaned up. The TTL dropped from one hour to five minutes to shrink the staleness window. Then the cache key was bumped from fearGreed:latest to fearGreed:latest:v2, so any fake value already sitting in Redis got evicted on deploy, no waiting for the TTL, no manual flush.

Once those three changes shipped, the gauge was back in sync with alternative.me. And the next time the API goes down, the site will honestly say the index is unavailable instead of telling a story about a greedy market.

The takeaway

  • A fallback that shows fake data is more dangerous than an error state. An empty widget is ugly; a lying number destroys trust.
  • Return null on failure and let the UI render an explicit unavailable state. A visible failure gets fixed, a hidden one does not.
  • Caching extends the lifetime of a wrong value. When repairing poisoned data, lower the TTL and bump the cache key so the old value evicts immediately.
  • Silent bugs are nearly impossible to catch from the inside. Compare the numbers on your site against the original source once in a while, because a screenshot from the client is not a monitoring system you want to rely on.