Caching and syncs
Feature data is cached at several layers. The client app stays fast, and refreshes skip origin when nothing changed.
The database is the source of truth. Caches only accelerate reads.
Why this matters
Customer apps often keep a tab open for hours. Without caching, each refresh would hit origin for a full payload even when no feature changed.
- Most refreshes come from memory or the edge.
304and edge hits skip full origin reads when features are unchanged.- Toggles propagate over live push. Worst-case delay follows short cache TTLs.
Cache layers
| Layer | Where | Typical TTL | What it stores |
|---|---|---|---|
| SDK memory | Client app process | Until next refresh | Resolved features + ETag |
| Edge CDN | Near end users | Up to 60s | Full 200 responses per API key |
| API server | FeatureToggle backend | Up to 30s | Resolved features per environment |
The SDK handles this automatically.
Origin requests vs cache hits
A full origin response is a GET /v1/features or GET /v1/features/:key that returns 200 with body.
Not a full origin read:
| Response | Why |
|---|---|
304 Not Modified | Features unchanged, no new data |
| Edge cache hit | Request never reached origin |
| Dashboard actions | Creating or toggling features in the UI |
With caching and SSE, apps that refresh often while features are stable mostly see 304 or edge hits instead of repeated full payloads.
Steady state: nothing changed
After a feature is toggled
How fast does the client app see the change?
| Path | Typical latency |
|---|---|
| SSE connected | Seconds |
| SSE disconnected | Next tab-focus refresh, poll tick (stream: "off"), or init() |
| Worst case (caches only) | Up to ~60s + refresh trigger |
SDK refresh triggers
| Trigger | When |
|---|---|
SSE features-changed | Primary path for a dashboard toggle (stream: "auto" or "notify") |
| Tab focus | User returns to the tab (when no poll timer is active) |
| Poll timer | Background interval when stream: "off" and pollInterval > 0 |
init() | SDK startup |
refresh() | Manual refetch |
Default production apps use stream: "auto" and run no background poll timer. Updates come from SSE, tab focus, and manual refresh().
With stream: "off", the SDK may run a background poll (default 30s). Set pollInterval: 0 to disable polling and use tab focus instead. Repeat polls send If-None-Match; unchanged features return 304 (not a full origin read).
Conditional requests (ETag / 304)
Only GET /v1/features supports conditional GET:
- First request →
200+ETagheader (environment version). - Later requests →
If-None-Match: "<version>". - Unchanged →
304(empty body). - Changed →
200+ new payload + newETag.
304 responses are never cached at the edge (Cache-Control: no-store).
Single-feature reads (GET /v1/features/:key) always return full data.
What is not cached
| Item | Reason |
|---|---|
GET /v1/features/stream | Long-lived SSE connection |
304 at the edge | Stale conditional response risk |
GET /v1/features/:key | No conditional GET |
Practical tips
- Use the SDK. It manages
ETag, SSE, and refresh timing. - Keep SSE connected in production. It is the fastest toggle-to-app path.
- Keep
stream: "auto"in production. SSE is faster than polling, so avoid running a tight custom poll loop alongside it. - Use
stream: "off"+pollIntervalonly when SSE is unavailable: tests, restricted runtimes, or proxies that block long-lived streams. - Evaluate sensitive features on the client server. Browser cache can be sub-minute stale without SSE, and authorization decisions need server-side checks.
FAQ
The client refreshes 100 times and nothing changed. How many full origin payloads is that?
Usually zero. The client gets 304 or edge cache hits instead.
Does toggling in the dashboard count as an API read?
No. Dashboard writes are separate from the feature API.
Force a fresh read?
Call refresh(). Changed features → 200. Unchanged → 304.