Skip to content

Caching & conditional requests

Read endpoints are cached server-side and support HTTP conditional requests. Well-behaved clients get large speedups for free — on the reference stack a cached read answers in ~3 ms vs ~190 ms uncached (~60×).

Server-side cache — X-Cache

List and search responses are cached in Redis for a short TTL, keyed by path + normalized query string (never by API key — the data is identical for every caller). Every response says what happened:

X-Cache Meaning
MISS Computed from the database, now cached
HIT Served from cache
BYPASS Never cached (export streams)

Freshness is eventual, bounded by the TTL: shortly after a re-ingest you may briefly see the previous rows. Source datasets update on the order of months, so in practice this is invisible.

Caching saves time, not quota

Auth, rate limiting, and metering run before the cache. A HIT (or a 304) is fast, but it still counts as a request. See Authentication & limits.

Conditional requests — ETag / 304

Every cacheable response carries a strong, content-derived ETag and Cache-Control: private, max-age=60. Re-validate with If-None-Match: if the content is unchanged you get a bodyless 304 — no payload to download or parse.

ETAG=$(curl -sI -H "Authorization: Bearer $API_KEY" \
  "https://api.canovix.ro/v1/economy/unemployment?limit=100" \
  | tr -d '\r' | awk -F': ' 'tolower($1)=="etag" {print $2}')

curl -i -H "Authorization: Bearer $API_KEY" -H "If-None-Match: $ETAG" \
  "https://api.canovix.ro/v1/economy/unemployment?limit=100"
# HTTP/1.1 304 Not Modified
class ConditionalGetter:
    """Cache bodies locally; refetch only when content actually changed."""

    def __init__(self, client: httpx.Client) -> None:
        self.client = client
        self.etags: dict[str, str] = {}
        self.bodies: dict[str, dict] = {}

    def get(self, url: str) -> dict:
        headers = {}
        if url in self.etags:
            headers["If-None-Match"] = self.etags[url]
        response = self.client.get(url, headers=headers)
        if response.status_code == 304:
            return self.bodies[url]
        self.etags[url] = response.headers.get("etag", "")
        self.bodies[url] = response.json()
        return self.bodies[url]
const etags = new Map();
const bodies = new Map();

async function conditionalGet(url) {
  const headers = { Authorization: `Bearer ${API_KEY}` };
  if (etags.has(url)) headers["If-None-Match"] = etags.get(url);
  const res = await fetch(url, { headers });
  if (res.status === 304) return bodies.get(url);
  etags.set(url, res.headers.get("etag"));
  bodies.set(url, await res.json());
  return bodies.get(url);
}

Compression

Responses over 1 KiB are gzip-compressed when the client advertises Accept-Encoding: gzip (curl: --compressed; httpx and fetch handle it automatically). Dataset JSON is highly repetitive — a typical 100-row page compresses ~15×, so keep compression on for anything but tiny requests.

Detecting data changes

Don't poll rows to see whether a dataset was re-ingested — ask /v1/datasets/{id}: latest_ingestion.version increments (and imported_at moves) on every successful ingestion. Poll that tiny response, and refetch data only when the version changes.