# GET handlers consider stale-while-revalidate caching

- **Pattern:** `ab-000254` (`ai-slop-cost-bombs.cache-idempotency.stale-while-revalidate-considered`)
- **Severity:** info
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000254
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000254)

## Why it matters

GET routes that return the same data on every request — product listings, published posts, public user profiles — without any caching strategy convert every user visit into a full database round-trip. At modest scale this is waste; at traffic spikes it is the mechanism by which a popular moment becomes a database overload event. Even a 60-second `stale-while-revalidate` cache cuts database load by roughly 100x for a route that serves one request per second, while keeping data within a minute of fresh. OWASP LLM10 (Unbounded Consumption) identifies uncached expensive operations as a key cost amplification vector — the same logic applies to database-backed GET routes that serve identical responses to many users.

## Severity rationale

Info because missing caching is an efficiency gap rather than a security or correctness defect — the application functions correctly, it just costs more to operate than necessary.

## Remediation

Add `Cache-Control: public, s-maxage=60, stale-while-revalidate=300` to read-only GET routes that serve public data. In a Next.js Route Handler:

```ts
export async function GET() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { publishedAt: 'desc' },
    take: 20,
  })
  return Response.json(posts, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
    },
  })
}
```

Alternatively, export `revalidate = 60` at the route segment level to use Next.js's built-in ISR. Reserve `cache: 'no-store'` for routes that return user-specific or time-critical data. Document the caching intent with a comment so future developers know the omission is intentional when it is.

## Detection

- **ID:** `stale-while-revalidate-considered`
- **Severity:** `info`
- **What to look for:** Walk all API handler files exporting `GET`. Count all GET handlers and verify whether each one sets cache headers (`cache-control`, `s-maxage`, `stale-while-revalidate`), uses `unstable_cache`, exports `revalidate`, or uses Next.js fetch caching. EXCLUDE handlers that legitimately need to be uncached (auth checks, real-time data).
- **Pass criteria:** At least 30% of GET handlers consider caching strategy (cache headers, revalidate, etc.). Report: "X GET handlers inspected, Y with caching strategy, Z uncached."
- **Fail criteria:** Fewer than 30% of GET handlers have any caching strategy AND the project has more than 5 GET handlers.
- **Skip (N/A) when:** Project has fewer than 5 GET handlers.
- **Detail on fail:** `"8 GET handlers, 1 with caching strategy. The other 7 have no Cache-Control headers and no Next.js revalidate — every request is a database hit"`
- **Remediation:** Even a 60-second cache cuts database load by 100x for popular routes. Add cache headers to read-only GET routes:

  ```ts
  export async function GET() {
    const data = await prisma.post.findMany({ where: { published: true } })
    return Response.json(data, {
      headers: {
        'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
      },
    })
  }
  ```

---

## External references

- iso-25010 performance-efficiency

Taxons: cost-efficiency, performance

HTML version: https://auditbuffet.com/patterns/ab-000254
