Caching & freshness
#Caching & freshness
Every /api/v1 data response is served from a server-side cache, refreshed at least once an hour
(3600 seconds by default — a club can be configured for a shorter window on race days, never a longer one).
Results can be up to that old. Four headers tell you exactly how old:
| Header | Meaning |
|---|---|
Cache-Control: private, max-age=<n> |
seconds left in the current server-side cache window — cache the response yourself for at least this long |
ETag: "<hash>" |
a strong hash of the exact body — send it back as If-None-Match next time |
Last-Modified |
when the underlying data last changed (list responses: the newest item) |
X-Data-As-Of |
ISO-8601 UTC timestamp (with milliseconds) of when this body was read — the "as of" time to show a viewer if you display it |
#Conditional requests
# first call
curl -s -D- https://sail-club-server.cloud.run/api/v1/events/EVENT_ID \
-H "Authorization: Bearer $TOKEN" -o event.json | grep -i etag
# ETag: "3f9a1c0d2b7e4a6f8c1d2e3f9a1c0d2"
# later — send it back
curl -s -o /dev/null -w '%{http_code}\n' https://sail-club-server.cloud.run/api/v1/events/EVENT_ID \
-H "Authorization: Bearer $TOKEN" \
-H 'If-None-Match: "3f9a1c0d2b7e4a6f8c1d2e3f9a1c0d2"'
# 304 — your cached copy is still correct, no body sentA matching If-None-Match returns 304 with no body — cheaper for both sides than re-downloading
unchanged JSON. It still counts as a call against your rate limit, the same as a 200 would — a conditional
request saves bandwidth, not rate-limit budget. See Rate limits.
#How fresh a change actually is
A change made in the panel (a race scored, a result corrected) reaches /api/v1 after: the internal read
model rebuilds (roughly 30 seconds, with a safety re-check at 5 minutes) plus whatever remains of the
current cache window (up to the full hour). There is no way to force an immediate refresh from the API side
— if a club needs sub-hour freshness during a regatta, that's a cache-window change on our side, not
something an integration can request per-call.
#Recommended integrator-side cache
Don't call /api/v1 live per visitor request. A simple, correct pattern:
on your own schedule (e.g. every 15 min, well under max-age):
call the endpoint with If-None-Match: <your stored ETag>
if 304: keep serving your existing cached copy, done
if 200: replace your cached copy and stored ETag with the new body
serve every visitor from your own cache/database, never from a live callThis also means a temporary outage on our side (503 API_UNAVAILABLE)
never has to be visible to your visitors — you already have the last good copy.