QRQR Code Agency
Concepts

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 encoded data_type + payload)
  • size_inches, dpi
  • color, 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 (png or svg)

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
ValueMeaning
MISSRendered fresh, took X-QR-Duration-Ms ms
HITServed from cache, ~0 ms

When the cache helps

ScenarioHit 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:

  1. Wait 24 hours. The entry expires and the next call re-renders.
  2. Change one parameter. Adding a query string to your URL (https://x.com?v=2) changes data, which changes the hash.
  3. Operator action. Open a support ticket with the X-Request-Id if 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: 0

What is next

On this page