# Pagination on list endpoints

- **Pattern:** `ab-002168` (`saas-api-design.request-response.pagination-list-endpoints`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002168
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002168)

## Why it matters

Unpaginated list endpoints are a denial-of-service waiting to happen: as data grows, `findMany({})` with no `take` fetches every record in the table into memory, exhausting database connection pool, server RAM, and response bandwidth simultaneously. This maps directly to CWE-770 (allocation of resources without limits) and OWASP API 2023 API4 (unrestricted resource consumption). A single authenticated request can trigger minutes of database load. Cost-per-request grows linearly with data, making unbounded lists a latent cost bomb in addition to a reliability risk.

## Severity rationale

High because even a single unbounded list query can exhaust database memory and connection pool, causing a denial-of-service condition that affects all users.

## Remediation

Add offset-based or cursor-based pagination to every list endpoint in `app/api/`. Accept `page` and `limit` query params, enforce a maximum page size, and include pagination metadata in the response:

```ts
const page = Number(searchParams.get('page') ?? 1)
const limit = Math.min(Number(searchParams.get('limit') ?? 20), 100)
const skip = (page - 1) * limit

const [items, total] = await Promise.all([
  db.post.findMany({ skip, take: limit }),
  db.post.count(),
])

return apiSuccess({ items, meta: { page, limit, total } })
```

For high-volume tables, replace offset pagination with cursor-based pagination (pass the last record's `id` as a cursor) to avoid offset drift and index scans.

## Detection

- **ID:** `pagination-list-endpoints`
- **Severity:** `high`
- **What to look for:** Enumerate all API endpoints that return lists or collections (endpoints where the handler queries for multiple records — look for `findMany`, `select *`, array returns, `.getAll()`, `.list()`, cursor-based queries). Check whether each list endpoint implements pagination. Acceptable pagination patterns: offset/limit (`?page=1&limit=20`), cursor-based (`?cursor=<id>&limit=20`), keyset pagination. Also check: is there a maximum limit enforced to prevent `?limit=999999`?
- **Pass criteria:** At least 100% of list endpoints (endpoints returning arrays of resources) implement some form of pagination. A hard limit on page size is enforced (even if it's just capping the maximum). Report even on pass: report the count of list endpoints found and pagination method used.
- **Fail criteria:** One or more list endpoints return the entire dataset with no pagination — handler queries `findMany({})` or equivalent with no `take`/`limit`/`skip` applied.
- **Skip (N/A) when:** No list endpoints exist (all routes operate on single resources). Signal: no `findMany`, `selectAll`, `.list()`, or array-returning queries found in any route handler.
- **Detail on fail:** Identify which endpoints lack pagination (e.g., "GET /api/posts returns all records with no limit; GET /api/users does the same. findMany() called without take/skip."). Max 500 chars.
- **Remediation:** Add pagination to every list endpoint in `app/api/` route handlers. The simplest approach is offset-based pagination: accept `page` and `limit` query parameters, apply them to the database query, and include pagination metadata in the response. For example in Prisma: `findMany({ skip: (page-1)*limit, take: Math.min(limit, 100) })`. Return `{ data: items, meta: { page, limit, total } }` so clients can implement pagination UI. For high-volume datasets, consider cursor-based pagination instead — it's more efficient and avoids offset drift. Always enforce a maximum `limit` to prevent clients from requesting unlimited data.

## External references

- cwe CWE-770
- iso-25010 performance-efficiency.resource-utilization

Taxons: cost-efficiency

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