Caching & Performance
How Crop Picker stays fast with Vercel edge caching: the per-route cache rules, how query strings are cached, stale-while-revalidate, region handling, invalidation, and what is not cached.
How Crop Picker stays fast and keeps Supabase traffic low — and why the caching strategy holds up as new regions come online.
#Caching layers, outermost first
| Layer | What it caches | TTL | Controlled by |
|---|---|---|---|
| Vercel CDN | SSR HTML + API JSON | 10 min – 24 h | routeRules in nuxt.config.ts (pages); server/plugins/api-cache-headers.ts (API) |
| Browser HTTP cache | Static assets (hashed) | 1 yr | Nuxt build pipeline |
| Nuxt useAsyncData | SSR payload, client nav | in-memory | Nuxt runtime |
There is no Redis. The only application-level cache is the matching engine's in-memory copy of the catalog, kept for 10 minutes per server instance, so /recommend, /systems/evaluate and /crops/:slug/fit can lag the database by up to 10 minutes. Otherwise the CDN layer is doing the heavy lifting; Supabase is only hit on cache MISSes or revalidations.
#Route rules
Two mechanisms are used, because Vercel's page cache (ISR) caches by path only and drops the query string.
Pages: ISR with a time limit (isr in nuxt.config.ts). One cached copy per path, regenerated after the lifetime.
- / → redirect to /crops
- /crops → 10 min
- /crops/:slug → 1 hr
- /about → 1 hr
- /interactions → 1 hr (a shared
?crops=selection is restored in the browser after the page loads) - /sitemap.xml → 1 hr
- /robots.txt → 24 hr
- /buyers, /sources, /match → not cached (filter state is in the query string)
API: CDN caching per full URL (server/plugins/api-cache-headers.ts). These routes run on origin and send Cache-Control: s-maxage, which Vercel's CDN caches separately for each URL including its query string, so every parameter combination gets its own correct response.
- /api/v1/meta → 24 hr
- /api/v1/regions → 1 hr
- /api/v1/data-sources → 1 hr
- /api/v1/crops/:slug, /crops/:slug/snowflake, /sources, /fit, /cultivars → 1 hr
Only successful responses are cached; errors are not.
API: never cached
- /api/v1/crops, /api/v1/crops/compare, /api/v1/buyers → filter and pagination params, served from origin
- /api/v1/audit-log → always fresh
- /api/v1/recommend, /api/v1/systems/evaluate → send
no-store - /api/v1/interactions, /api/v1/interactions/*, /api/v1/regions/presets → served from origin
#Stale-while-revalidate semantics
After a cached response reaches its lifetime:
- A request comes in → CDN serves the stale cached response immediately (zero wait for the user)
- The CDN fires an async revalidation request to the origin in the background
- When the fresh response lands, it replaces the stale copy for subsequent requests
For the API routes the stale copy can be served for at most 5 minutes past its lifetime (stale-while-revalidate=300).
The user never waits on a revalidation. The only slow requests are true cache MISSes (first hit per edge region, or after a deploy).
#Verifying the cache is live
Run: curl -sI https://crops.every.farm/api/v1/meta
Look at the x-vercel-cache and age headers. After the second request to the same edge node you should see x-vercel-cache: HIT, and age (seconds since the copy was cached) should never exceed the route's lifetime. Vercel removes the s-maxage directive before the response reaches you, so the visible cache-control reads public, max-age=0; that is expected.
#Region handling
A brief on how the cache stays correct across users in different regions.
Problem: every visitor hits the same URL (e.g. /crops/elderberry) but may be in a different region. If the CDN cached a region-specific payload, users in other regions would see the wrong snowflake scores.
Solution: the server always renders with the platform's default region. The CDN caches that single payload per URL. After hydration, useUserLocation reads the user's region from localStorage and, if it differs from the default, the region-aware composables (useCropDetail, useCrops, useCropComparison, useNearbyBuyers) refetch client-side via their watch hooks on regionKey.
Trade-off: a brief (~100–300 ms) window after hydration where a non-default-region user sees default-region data. Functional correctness is preserved; the flash is cosmetic.
Net effect for adding regions: zero cache config changes. Adding a row to regions + running the ingestion for that region is sufficient. The picker auto-updates, client-side refetches take care of data correctness.
#Invalidation
Every deploy purges the edge cache globally, and each cached route also expires after its lifetime above. For the platform's ingestion cadence (data updates on the order of days, not seconds) this provides a predictable freshness window for consumers.
#What is NOT cached
- /api/v1/audit-log — served from origin on every request. Admin and AI-agent consumers need real-time changes.
- Any client-side Supabase query (they bypass Vercel entirely, going direct to Supabase). If a composable's data becomes hot enough to matter, move that read through /api/v1/*.
- First request per edge region after deploy (always MISS; normal and harmless).
#Summary
For consumers of the public API, this means most requests resolve at the CDN edge in under 50 ms globally, and the platform behaves consistently under traffic spikes because origin load is decoupled from request volume.
Updated
Something missing or out of date? Tell us — the docs are updated with every release.