Apexon BLOG

PLATFORM

X-Cache: HIT — why our APIs tell you when data is stale

Most data APIs cache aggressively and never say so. Every Apexon response carries a header that tells you exactly what happened — served from a stored copy, or fetched from upstream just now — so you never have to guess how fresh what you're looking at actually is.

Apexon team··9 min read

Honest API caching means the response itself tells you, on every single call, whether what you received was pulled from a stored copy or fetched from upstream just now — not a guess, not a marketing claim about "real-time" data, just a header with one of two values. Apexon caches hard, because caching is what makes fast, affordable market and catalog data possible at any real scale, and every response says so in plain sight: X-Cache: HIT when you got a cached copy, X-Cache: MISS when we went and fetched it for you. Caching isn't the dishonest part of most APIs — hiding it is, and that's the one thing we don't do.

That header is one of a small, fixed set that rides on every response, and together they answer the questions you'd otherwise have to guess at: how fresh is this, how close am I to my rate limit, which plan did my key actually resolve to, and who do I reference if something looks wrong. None of it is locked behind a support ticket — it's in the headers, on every call, for free.

The headers on every response

These are the exact header names the request-handling middleware sets — verified against the code, not copied from a slide:

HeaderWhat it tells you
X-RateLimit-LimitYour per-minute request budget on the plan your key resolved to.
X-RateLimit-RemainingHow many requests are left in the current one-minute window — this decrements on every request, cache hit or not.
X-RateLimit-ResetSeconds until the current window rolls over and your remaining count resets.
X-CacheHIT or MISS on every response from this API — whether this specific answer came from a stored copy or a call we made upstream just now.
X-Request-IdA unique id minted fresh for this one request, even on a cache hit — the value to paste into a support email so we can find exactly what happened.
X-PlanThe plan your key actually resolved to, server-side (public on routes that allow unauthenticated calls) — trust this over whatever you remember signing up for.
X-Response-Time-msHow long this server spent building this one response, in milliseconds — a real per-request number, not an average or a promise about the next one.

Notice what's not there: no Age header. HTTP's built-in caching signal reports how many seconds a shared cache has been holding a stored response — useful when you don't control the origin. We do control the origin, so we tell you something more direct instead: X-Cache says whether this response is a stored copy at all, and your own request timestamps tell you how long it's been since you last asked. Between those two, an Age header wouldn't add information — it would just be one more field to reconcile.

One qualification, because this whole post is about not overstating things: HIT and MISS are the complete set of X-Cache values this API's request-handling code ever sets — there's no hidden third state in this codebase. That's not a platform-wide promise, though. A product's caching layer can define additional states for its own failure modes — a bounded-age fallback response served instead of a hard error during an upstream outage, say, would reasonably carry a different X-Cache value than either of these two. Don't assume every Apexon response uses this exact two-value table; check the product you're actually calling (more on that in the FAQ).

What honest caching looks like in practice

Call the same quote endpoint twice in a row and you can watch this work instead of taking it on faith:

curl -si "https://api.apexon.dev/v1/market/quotes?symbols=AAPL,MSFT" \
  -H "X-API-Key: fm_live_XXXX"

The first call is a MISS — nothing was cached yet for that exact query, so the request went to the upstream source before answering you:

X-Cache: MISS
X-Request-Id: 4c2f19a0e8b7d341
X-Plan: pro
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 47
X-Response-Time-ms: …

Call it again immediately, same symbols, same query string, and — as long as the cache window for that route hasn't rolled over — you get:

X-Cache: HIT
X-Request-Id: 9a71c630f4d28e56
X-Plan: pro
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 46
X-Response-Time-ms: …

(X-Response-Time-ms is a real value on both of those responses — we're leaving it as here on purpose rather than printing a number that would look like a performance claim. See the next section for why.)

Two things worth noticing in that second response. X-Request-Id is different — a fresh id gets minted for every request regardless of cache status, because it identifies the request, not the answer. And X-RateLimit-Remaining still went down by one: the per-minute limit is checked and counted before the cache lookup happens, so a cache hit costs you rate-limit headroom even though — unlike a cache miss — it doesn't count against your monthly quota. Two different meters, two different rules; the headers are what let you actually see which one moved.

What should a client do with a HIT versus a MISS? For anything you're displaying to a person — a dashboard, a watchlist, a summary page — a HIT is fine to render as-is; data behind a quotes response doesn't move fast enough within one short cache window for the difference to matter to someone reading a screen. For code that acts on the number rather than showing it — placing an order, checking a limit, triggering an alert — don't build logic that assumes a HIT and a MISS carry the same freshness guarantee. Treat a cached answer as accurate up to that endpoint's cache window, not as of this instant, and if your logic needs a tighter guarantee than that window allows, that's a conversation with us about the right route for it — not something to route around by hammering the endpoint until you happen to land on a MISS.

Why we don't publish latency numbers

Because we haven't measured them the way a number that goes in marketing copy needs to be measured — under realistic concurrent load, across plans, across the actual mix of endpoints customers call, over enough time to say something is typical rather than a lucky sample. X-Response-Time-ms is real and it's on every response, so you can measure your own traffic against your own workload starting right now. What we won't do is take one number from one test and print it on a page as if it describes what you'll see.

Platform policy. An unmeasured latency number is marketing, not engineering — we don't publish one until it's backed by a real methodology. Until then, the X-Response-Time-ms your own calls actually produce is the only latency figure worth trusting.

The same logic applies to uptime. There's no published SLA today, for the same reason: a percentage without a monitoring methodology behind it is a guess dressed up as a commitment. What exists instead is a live status page showing current availability, and the X-Response-Time-ms your own calls actually produce — both closer to ground truth than a figure we'd otherwise have to make up.

Client patterns

Three habits cover most of it.

Read X-RateLimit-Remaining before you fire the next request, not after you get a 429. A client that only reacts to a rejected request is always one request behind; back off as remaining approaches zero and you'll rarely see a 429 in production traffic. When you do hit one, respect the Retry-After header on the error response — the number of seconds to wait — rather than retrying on a fixed interval of your own choosing.

Log X-Request-Id on every response your client receives, and include it verbatim in any support email. It's the fastest way to turn "something broke this morning" into "here's exactly what happened on request 4c2f19a0e8b7d341," because it's the same id we can look up server-side.

Treat X-Plan as the source of truth for which tier you're actually operating under, not whatever you remember signing up for. Plan changes, key rotations, and marketplace purchases all resolve server-side; if a rate limit looks tighter than expected, check X-Plan on the response before assuming something's wrong on our end.

FAQ

Does caching mean my data is old?

It means the response may have been served from a recent copy rather than fetched from upstream at the exact moment you asked — not that anything is wrong or unusually stale. X-Cache tells you which happened, on every single response: MISS if we called upstream for you just now, HIT if you got a stored copy from within that endpoint's cache window.

What happens when I hit the rate limit?

You get a 429, plus the same X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers you'd see on a successful call — Remaining reads 0 — along with a Retry-After header telling you how many seconds to wait. Respect that value instead of retrying on your own schedule; hammering the endpoint while Remaining is 0 won't get you an answer any faster. If the whole service is under load rather than just your key being over budget, you may see a 503 instead — a different signal (retry shortly) than a 429 (you're over your own limit).

Is there an SLA?

No — there's no published SLA today. The status page at apexon.dev/status.html shows current availability for each product, which is the accurate picture of what's up right now. Treat it as the source of truth, not as an implied guarantee we haven't actually made.

Do all Apexon products use these headers?

The convention — X-Request-Id, X-Plan, and rate-limit headers on every response — is platform-wide. But the exact set, and any additional states a header can carry (this post already flags that X-Cache isn't guaranteed to stay a strict two-value field everywhere), is documented per product rather than assumed uniform. Check the reference for the product you're actually integrating instead of assuming this page's table applies byte-for-byte to another one.

See the shared conventions across every product

Auth headers, rate-limit headers, and the error envelope are documented once, platform-wide — then each product's reference lists its exact header set and endpoints.

Platform conventions More posts