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.