Cache brand packs in your own backend
Reduce uncached-call costs by caching brand packs server-side. brandRNA already caches at $0/cached-call but local cache adds latency wins.
brandRNA caches brand packs internally at $0 per cached call, with a 24h TTL. That's already cheap. But every cache hit still costs you ~80ms of HTTP round-trip latency — non-trivial if you're personalising every pageview at 1k+ RPS. A local cache in your own backend cuts that to a few millisecond DB or Redis lookup.
This recipe shows when to cache, what to cache, and how to invalidate.
When it's worth it
| Use case | Local cache? | Why |
|---|---|---|
| One-off scripts | No | $0 cached + simplicity wins. |
| Server-rendered landing pages | Maybe | Worth it if p99 latency matters. |
| 1k+ RPS personalisation | Yes | 80ms → 2ms is meaningful at scale. |
| Multi-region deployment | Yes | Avoid cross-region brandRNA hops. |
| Edge functions | Yes (KV / D1) | Keep brand packs co-located with your renderer. |
Recommended TTL
Match brandRNA's own cache: 24 hours. Rebrands and redesigns are rare; a daily refresh is more than fresh enough for most use cases.
If you operate in a fast-moving vertical (crypto, fashion, news) and care about same-day rebrands, drop the TTL to 6h. There's no advantage to going below brandRNA's 24h ceiling — your cache will just hit the brandRNA cache, which is still 24h old.
Schema suggestion
CREATE TABLE brand_pack_cache (
domain text PRIMARY KEY,
pack jsonb NOT NULL,
fetched_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
source_cached boolean NOT NULL -- whether brandRNA returned cached:true
);
CREATE INDEX ON brand_pack_cache (expires_at);KEY: brandpack:{domain}
VALUE: <serialized JSON pack>
TTL: 86400 (24h)Use SET key value EX 86400 to atomically write + expire.
For Cloudflare KV / Workers D1 / Vercel Edge Config:
await KV.put(`brandpack:${domain}`, JSON.stringify(pack), {
expirationTtl: 86400,
});Implementation
Read-through cache
const KEY = process.env.BRANDRNA_API_KEY!;
export async function getCachedPack(domain: string) {
const cached = await db.query.brandPackCache.findFirst({
where: eq(schema.brandPackCache.domain, domain),
});
if (cached && cached.expiresAt > new Date()) {
return cached.pack;
}
const fresh = await fetchFromBrandRNA(domain);
await db.insert(schema.brandPackCache).values({
domain,
pack: fresh,
expiresAt: new Date(Date.now() + 86400_000),
sourceCached: fresh.metadata.cached === true,
}).onConflictDoUpdate({
target: schema.brandPackCache.domain,
set: {
pack: fresh,
fetchedAt: new Date(),
expiresAt: new Date(Date.now() + 86400_000),
},
});
return fresh;
}
async function fetchFromBrandRNA(domain: string) {
const res = await fetch(
`https://api.brandrna.com/api/v1/pack/${domain}`,
{ headers: { Authorization: `Bearer ${KEY}` } },
);
if (!res.ok) throw new Error(`brandRNA ${res.status}`);
return res.json();
}Python equivalent
import os
import time
import httpx
KEY = os.environ['BRANDRNA_API_KEY']
TTL = 86400 # 24h
async def get_cached_pack(domain: str, db, cache) -> dict:
cached = await cache.get(f"brandpack:{domain}")
if cached:
return cached
async with httpx.AsyncClient(timeout=30) as client:
r = await client.get(
f"https://api.brandrna.com/api/v1/pack/{domain}",
headers={"Authorization": f"Bearer {KEY}"},
)
r.raise_for_status()
pack = r.json()
await cache.set(f"brandpack:{domain}", pack, ex=TTL)
return packInvalidation
Two triggers worth wiring up:
- Manual override. Expose an admin endpoint that drops the cache row for a single domain — useful when you spot a rebrand before our cache rolls over.
- brandRNA
metadata.cached: false. When this field isfalse, brandRNA just performed a fresh extraction. That's a strong signal the domain changed, so refresh proactively.
if (fresh.metadata.cached === false) {
// brandRNA re-extracted: cache it locally with full TTL
await db.update(schema.brandPackCache)
.set({ pack: fresh, fetchedAt: new Date(), expiresAt: new Date(Date.now() + 86400_000) })
.where(eq(schema.brandPackCache.domain, domain));
}Don't cache 404 / 5xx responses. Failures are usually transient (Cloudflare interstitial, scraper timeout) — cache them and you'll serve stale errors for a full TTL.
What's next
- Combine with Personalize a landing page for the full personalisation flow.
- Read rate limits to size your TTL against your monthly quota.