Improving API Performance with Response Caching (PERF-12)
Improving API Performance with Response Caching (PERF-12)
Version: 0.1.80
Category: Performance — Server
Background
The sanctions screening platform exposes several API routes that serve data consumed by the monitoring dashboard and the OFSI list change feed. Until v0.1.80, every one of these routes carried export const dynamic = 'force-dynamic' — a Next.js directive that disables all framework-level caching — and none of them set Cache-Control headers on their HTTP responses.
The result: a new database query on every request, for every user, even when the underlying data hadn't changed in minutes or hours.
The Problem
force-dynamic applied universally
Next.js's force-dynamic is appropriate for routes that must always reflect real-time data (e.g. a live transaction feed). It is not appropriate for routes whose data changes on a predictable, infrequent schedule. Applying it globally adds unnecessary database load with no user-facing benefit.
No HTTP cache headers
Without Cache-Control headers, browsers, CDN edges, and reverse proxies cannot cache responses even for a few seconds. Every page load or component re-render that hits these endpoints results in a round-trip all the way to the database.
The Fix
/api/dashboard/stats
This endpoint returns aggregate counts (e.g. total screenings, alerts by status). The data is:
- Per-user — each compliance user sees their own team's figures.
- Infrequently changing — counts shift as screenings are processed, but stale-by-one-minute data is acceptable for a monitoring dashboard.
Cache-Control: private, max-age=60, stale-while-revalidate=300
| Directive | Value | Meaning |
|---|---|---|
private | — | Response must not be stored by a shared cache (CDN, proxy). Prevents one user's stats leaking to another. |
max-age | 60 s | Browser considers the response fresh for 1 minute. |
stale-while-revalidate | 300 s | Browser may serve a stale response for up to 5 minutes while fetching a fresh one in the background. |
/api/sanctions/changes
This endpoint lists changes to the OFSI consolidated sanctions list. The data is:
- Not user-specific — all users see the same list.
- Updated nightly — the automated sync runs once per day; intra-day staleness of a few minutes is inconsequential.
Cache-Control: public, max-age=300, stale-while-revalidate=3600
| Directive | Value | Meaning |
|---|---|---|
public | — | Response may be stored by shared caches (CDN, reverse proxy). Safe because data is not user-specific. |
max-age | 300 s | Response is considered fresh for 5 minutes. |
stale-while-revalidate | 3600 s | Stale responses may be served for up to 1 hour while a background refresh is in flight. |
Removing force-dynamic
Routes that do not require per-request dynamic behaviour have had export const dynamic = 'force-dynamic' removed, allowing Next.js's default caching strategy to apply.
What Stays the Same
- Routes that genuinely serve real-time or write-path data retain
force-dynamicas appropriate. - Per-user data is always marked
private— no user's data is ever stored in a shared cache. - The nightly OFSI sync schedule is unchanged;
/api/sanctions/changeswill reflect the latest sync within the stale-while-revalidate window.
Summary
| Route | Cache Scope | Fresh For | Stale-While-Revalidate |
|---|---|---|---|
/api/dashboard/stats | Private (per-user) | 60 s | 300 s |
/api/sanctions/changes | Public | 300 s | 3600 s |