# Next 15 API Routes Got Slower? Check Dynamic

> API routes feeling slower on 15: caching defaults flipped, every call live. Mark static explicitly, keep dynamic honest. Fix inside.

Source: https://boringpush.com/blog/nextjs-15-api-routes-slow-dynamic-fix

## My API got slower by upgrading

Same endpoints, same database, Next 15: p95 up 40%. No code changed (checked three times), no traffic spike, no deploy difference. Just slower — across the board, as if every route had quietly agreed to take longer.

They had. The caching-defaults flip (fetch no longer ambient-cached) rippled into API routes: responses previously served from cache layers now executed live per call — database hits, recomputation, full latency, every request. My "upgrade" had silently undisabled caching everywhere simultaneously.

Nextjs 15 api slow isn't degradation. It's defaults doing exactly what the changelog said, to routes nobody re-marked.

## Ambient vs explicit API caching

| Route class | Next 14 behavior | Next 15 behavior |
|---|---|---|
| GET static-ish data | Cached (fast, sometimes stale) | Live (correct, slower) |
| GET per-user data | Cached-ish (risky!) | Live (correct by accident) |
| POST/mutations | Live (correct) | Live (unchanged) |
| Revalidated paths | Worked ambiently | Need explicit config |
| Developer awareness | Zero (magic) | Required (no magic) |

Three details make it sting asymmetrically:

1. **Slowness arrives without errors.** No red, no warnings, no failed builds — just p95 drifting upward across weeks, noticed via user complaints or chance dashboard glances. Silent performance regressions bypass every gate except latency monitoring (which most solos lack). Invisible decay, visible churn.
2. **Correctness improved while speed died.** Per-user routes accidentally cached under 14 (stale-data bugs!) now run correctly live — the flip fixed real bugs while introducing slowness. Teams experience it as pure loss because the fixed bugs were invisible (users never report bugs they didn't notice) while slowness is felt daily.
3. **Blanket re-caching reintroduces old bugs.** The naive fix (cache everything explicitly) restores the stale-per-user-data bugs 14 had — trading slowness for wrongness. Each route needs its own verdict (cacheable vs live), made deliberately. Blanket anything is how both failures happened.

The fetch-cache post audited fetch calls. This one audits the routes serving them.

## Nextjs 15 api slow triage

Classify routes, mark deliberately, monitor p95 — slowness with a map, fixed by route.

**Step 1 — Classify every API route by cacheability.**

```bash
# inventory (15 min): list routes, mark each STATIC / LIVE / MIXED
grep -rn "export async function GET" app/api/ | while read r; do echo "CLASSIFY: $r"; done
# STATIC: catalogs, content, configs (same for all) | LIVE: user data, carts, auth-adjacent
# MIXED: split into static + live sub-routes (never one route both — that's the old bug)
```

Mixed routes are the trap: one endpoint serving static content plus user data can't be correctly cached OR correctly live — split them (public part cacheable, private part live, two routes). Classification is product judgment (what varies per user?), not technical trivia. Decide deliberately now or debug mysteriously later.

**Step 2 — Mark static explicitly, instrument the live.**

```js
export const dynamic = "force-static";   // cacheable: built/served statically
export const revalidate = 3600;          // freshness contract: hourly, explicit
// LIVE routes: export const dynamic = "force-dynamic" (honest, metered knowingly)
```

Explicit marks survive future flips (intent stated, defaults irrelevant) and review cleanly (unmarked routes fail review). Revalidate windows encode business tolerance (catalog hourly, content daily, prices never-cached). Live routes get latency budgets instead (p95 alert thresholds) — honest dynamic with monitoring beats accidental dynamic with surprise.

**Step 3 — Watch p95 per route, not averages.**

```bash
# per-route p95 tracking (logs + simple aggregation, or APM if budgeted):
# alert: route p95 up 50% week-over-week (regression signal, not noise)
# review: monthly latency table per route class (STATIC fast? LIVE justified?)
```

Averages hide regressions (p50 steady while p95 doubles — the classic silent decay shape). Per-route p95 catches exactly what this flip caused: broad, moderate slowdowns invisible in means. Alert on relative change (50% WoW) not absolute (routes differ legitimately). Monitoring that would have caught this in week one costs an afternoon to build.

That's the pattern — routes marked, slowness mapped.

## My API p95s, before and after marking

| Route class | Unmarked (15 default) | Marked deliberately |
|---|---|---|
| Static catalog | 380ms (live, wasteful) | 25ms (cached) |
| User orders | 420ms (live, correct) | 410ms (live, budgeted) |
| Mixed (split) | 500ms + stale bugs | 30ms static + 400ms live |
| p95 overall | +40% vs 14 | -10% vs 14 (better!) |
| Review rule | None | Unmarked fails review |

BoringPush routes declare their caching.

Being straight: explicitness costs classification afternoons (inventory + verdicts + marks) plus ongoing review friction (new routes need verdicts). Priced against 40% slower APIs plus returning stale-data bugs: marks are cheap, drift is expensive, and the marked state outperforms 14 (deliberate beats ambient — caching where correct, live where honest).

## FAQ

### Is nextjs 15 api slow fix just re-adding what 14 did automatically?

No — deliberately better: 14 cached indiscriminately (including per-user data — bugs), marked-15 caches discriminately (static fast, live honest). Restoration would reintroduce staleness bugs; intention fixes both slowness AND correctness. Don't restore ambient; install deliberate. The past wasn't golden, just fast-looking.

### How do route handlers interact with fetch caching inside them?

Composition: handler-level marks (force-static/dynamic) frame, inner fetch marks refine (force-cache/no-store per call). Mismatches fail safe-ish (dynamic handler + cached fetch = fresh handler, cached sub-call — review the combination). Audit both layers together (handler mark + inner fetch marks as one unit). Layers compose; audit composition, not layers alone.

### Should middleware/proxy cache API responses at the edge?

Selectively: cacheable GETs with stable URLs (catalog, content) gain edge speed massively; authenticated routes must never edge-cache (leakage across users — catastrophic). Edge rules keyed on path + auth-absence, reviewed quarterly. Edge is the fastest cache and the most dangerous — misconfiguration serves user A's data to user B at light speed.

### Do Server Actions need the same classification?

Actions mutate (live by nature, no cache marks apply), but their revalidation targets need the audit (stale tags after classification changes). Classify reads (routes + fetches); verify mutation-revalidation pairs per PR. Writes invalidate; reads cache; pairs must agree. Review the pair, never the half.

### Can agents do route classification reliably?

Inventory yes (list all routes mechanically), verdicts with guidance (static-vs-live heuristics plus product questions answered by you), marks reliably after verdicts. Agents classify patterns well and product judgment poorly — split accordingly (mechanical census to agents, staleness-tolerance verdicts to humans). Census tireless, judgment human, marks joint.

### When is "live everywhere" actually acceptable?

Low-traffic internal tools (admin panels, staging dashboards: correctness over speed, users patient), plus genuinely real-time data (live prices, live inventory, live collaboration). Accept explicitly (marked force-dynamic WITH latency budgets and monitoring), never by default-drift. Chosen live beats defaulted live — same latency, opposite sleep quality.

## Keep reading

- [Next 15 Fetch Lies by Default: Fix It](/blog/nextjs-15-fetch-not-cached-anymore) — the fetch half of this flip
- [14 to 16 Upgrade: Caching Broke Everything](/blog/nextjs-14-to-16-upgrade-cache-pain) — the corridor both halves belong to
- [0.1ms vs 35ms: The Latency Math That Picks SQLite](/blog/sqlite-100k-ops-latency-truth) — the leg math underneath API times
