Caching
How the QR Code Agency render cache works. Content-addressed keys, 24 hour TTL, and how to bust the cache when you need to.
Caching
Identical render requests get cached for 24 hours. Second-and-later calls return instantly without re-running the render pipeline. You still spend quota (the deliverable is what is billed), but the latency drops from ~300 ms to ~0 ms and our infrastructure stays cool under spike load.
How the cache key works
We hash every parameter that affects the output:
data(or the encodeddata_type+payload)size_inches,dpicolor,pattern,background- Eye styling fields (
eye_shape,eye_color, etc.) - Gradient stops (
gradient_from,gradient_to) - Frame fields (
frame_style,frame_color,frame_label, etc.) - Logo fields (
logo_clear_zone,logo_neon_alpha,logo_size_ratio) - The logo bytes (SHA-256) if any
- The background image bytes (SHA-256) if any
- Output
format(pngorsvg)
Anything that does not affect the output (your API key, request IP, timestamps, request id) is not in the key. Different keys hitting the same parameters share cache entries.
flowchart LR
A[Request] --> B[Hash params + logo bytes]
B --> C{In cache?}
C -->|Yes| D[Return cached bytes<br/>X-QR-Cache: HIT]
C -->|No| E[Render]
E --> F[Store with 24h TTL]
F --> G[Return bytes<br/>X-QR-Cache: MISS]Reading the response header
Every successful response carries:
HTTP/1.1 200 OK
Content-Type: image/png
X-QR-Cache: HIT
X-QR-Duration-Ms: 0
X-QR-Plan: starter
X-QR-Quota-Remaining: 487| Value | Meaning |
|---|---|
MISS | Rendered fresh, took X-QR-Duration-Ms ms |
HIT | Served from cache, ~0 ms |
When the cache helps
| Scenario | Hit ratio |
|---|---|
| Marketing campaign, same QR rendered for 10 000 page views | ~99% |
| Sticker batches with identical params | ~99% |
| Bulk endpoint with duplicate items | ~99% on duplicates |
Per-customer QRs (different data per user) | ~0% |
| Live preview UI where users tweak colors | ~50% steady state |
TTL and eviction
- Default TTL: 24 hours (86 400 s).
- Backend in production: Redis (cluster, replicated).
- Backend in development: Django LocMem cache (in-process, wiped on server restart).
- Eviction policy: Redis LRU once the configured memory cap is reached.
Invalidating the cache
There is no manual flush endpoint. Three ways to bypass a stale cache:
- Wait 24 hours. The entry expires and the next call re-renders.
- Change one parameter. Adding a query string to your URL
(
https://x.com?v=2) changesdata, which changes the hash. - Operator action. Open a support ticket with the
X-Request-Idif you need an immediate flush; we can scope it to a single key prefix.
A per-key flush endpoint is on the roadmap for Phase 4 along with a
Cache-Control: no-cache request header opt-out.
Privacy and the shared cache
The cache is content-addressed, not user-scoped. If two different customers happen to ask for the exact same QR (same URL, same size, same color, no logo), they share a cache entry.
This is safe because:
- The output is purely a function of public parameters.
- We never store API keys in the cache key.
- Logo bytes are part of the key, so your custom logos are not shared across keys.
- Background image bytes are part of the key for the same reason.
If you want to guarantee a unique cache slot, add a unique parameter
to your data:
{ "data": "https://x.com?campaign=abc", "size_inches": 4 }vs.
{ "data": "https://x.com?campaign=xyz", "size_inches": 4 }Both render the same visually, but they hit different cache slots.
What does not affect the key
These fields are intentionally excluded so they do not fragment the cache:
X-Api-Key(whose key)- Request IP
- Request id
- Workspace id
- Plan tier (
free,starter, etc.) - Timestamps
Two different keys, two different plans, same payload -> one cache entry.
Verifying a cache hit in production
A simple way to verify your integration is benefiting from the cache:
# First call: cache miss
curl -X POST https://api.qrstudio.agency/api/v1/generate/ \
-H "X-Api-Key: smk_..." \
-H "Content-Type: application/json" \
-d '{"data":"https://example.com","size_inches":4}' \
-o /dev/null -D - 2>/dev/null | grep -i "x-qr-cache"
# X-QR-Cache: MISS
# X-QR-Duration-Ms: 312
# Second call, same params: cache hit
curl -X POST https://api.qrstudio.agency/api/v1/generate/ \
-H "X-Api-Key: smk_..." \
-H "Content-Type: application/json" \
-d '{"data":"https://example.com","size_inches":4}' \
-o /dev/null -D - 2>/dev/null | grep -i "x-qr-cache"
# X-QR-Cache: HIT
# X-QR-Duration-Ms: 0What is next
- PNG vs SVG: pick the right output format
- Bulk generation: the cache shines on duplicate batches
- API reference