Improving API Performance with Response Caching (PERF-12)
Improving API Performance with Response Caching (PERF-12)
Released in v0.1.86
Background
The sanctions screening platform exposes several API routes consumed by the monitoring dashboard and compliance workflows. Prior to this release, every route used Next.js's force-dynamic export, which completely disables caching and issues a live database query on every request.
For routes like /api/dashboard/stats — which aggregates counts that change at most a few times per hour — and /api/sanctions/changes — which reflects a list updated once nightly — this was unnecessarily expensive. Under moderate load, each page refresh triggered redundant DB reads with no benefit to data freshness.
What Changed
/api/dashboard/stats
This endpoint returns per-user aggregate statistics (total screened, matches found, pending reviews, etc.). Because the data is user-specific, any caching must be scoped privately to prevent one user's cached response from being served to another.
The response now carries:
Cache-Control: private, max-age=60, stale-while-revalidate=300
| Directive | Value | Meaning |
|---|---|---|
private | — | Cache is bound to the individual user; not stored by shared proxies |
max-age | 60 s | Client considers the response fresh for 1 minute |
stale-while-revalidate | 300 s | Client may serve stale data for up to 5 minutes while fetching a fresh copy in the background |
/api/sanctions/changes
This endpoint lists changes to the OFSI consolidated sanctions list. The underlying data is refreshed by the nightly sync job, so sub-minute freshness is not required.
The response now carries:
Cache-Control: public, max-age=300, stale-while-revalidate=3600
| Directive | Value | Meaning |
|---|---|---|
public | — | Response may be stored by shared caches (CDN, reverse proxy) |
max-age | 300 s | Response is considered fresh for 5 minutes |
stale-while-revalidate | 3600 s | Stale response may be served for up to 1 hour while revalidating |
force-dynamic Removal
Routes that do not require a live DB read on every request have had export const dynamic = 'force-dynamic' removed. This allows Next.js to apply its standard caching heuristics, reducing unnecessary compute on the server.
Data Safety Considerations
- User data isolation is preserved by using
privateon all user-scoped endpoints. A shared cache will never store or serve another user's dashboard statistics. - Sanctions list accuracy is not materially affected. The nightly sync is the authoritative update mechanism; a 5-minute cache window does not delay actionable compliance information.
- Cache TTLs are deliberately conservative relative to actual data-change frequency.
Summary
| Endpoint | Before | After |
|---|---|---|
/api/dashboard/stats | No cache, DB query every request | private, 60 s fresh / 5 min SWR |
/api/sanctions/changes | No cache, DB query every request | public, 5 min fresh / 1 hr SWR |
| Other routes | force-dynamic | Next.js default caching |