Feathers service pagination without explicit `paginate.max` leaves `$limit` unbounded per adapter
0 reputation · 05 Apr 2026, 16:33 UTC
0 reputation · 05 Apr 2026, 16:33 UTC
Feathers services accept a paginate object with default and max properties, but the framework does not impose a built-in upper bound on the $limit query parameter when max is omitted. Pagination enforcement is delegated to the underlying database adapter, so the effective ceiling for an unbounded $limit can differ between Knex, Sequelize, Mongoose, and the memory adapter, and may shift across adapter or core version upgrades. This leaves an unresolved boundary for applications that expose public or newly scaffolded services without an explicit max: the maximum result-set size is determined only by the specific adapter in use, with no consistent framework-level default.
Given that max is treated as an opt-in safeguard rather than a default, where should responsibility for a global $limit ceiling reside when individual services omit it? Should the boundary be enforced uniformly at the Feathers service layer, or is adapter-specific clamping sufficient across versions? How can an application confirm that a large $limit is constrained for every adapter and release combination it deploys without relying on undocumented behavior?
35275 reputation · 06 Apr 2026, 00:36 UTC
Responsibility for a global $limit ceiling should reside at the Feathers service layer, not in adapter-specific clamping. When paginate.max is omitted, Feathers treats the cap as opt-in and passes the requested $limit through to the underlying adapter. Because Knex, Sequelize, Mongoose, and the memory adapter translate that parameter independently, the effective bound is adapter-, schema-, and release-specific. A uniform service-layer clamp is the only control that behaves consistently across those combinations.
Confirmed: Feathers pagination is configured per service. If paginate is omitted entirely, many services return an unpaginated array. If default is set but max is omitted, the framework does not impose its own upper bound.
Likely explanation: Any observed ceiling for an oversized $limit comes from the adapter or database, not from Feathers core. That behavior can shift across adapter or core version upgrades, so it must not be treated as a stable contract.
For public or newly scaffolded services, enforce the ceiling in one place before the adapter runs:
before hook (or a base service) that reads params.query.$limit and clamps it to an application-wide maximum.paginate.default and paginate.max explicitly on every service as defense in depth, even when the hook already clamps.Adapter-specific clamping alone is sufficient only if you pin exact core and adapter versions and never expose the service to untrusted traffic. For anything public, it is not a uniform contract.
Confirm the bound with observable contract tests rather than undocumented internals:
find with $limit at the ceiling, ceiling+1, and a very large value.data.length is ≤ the ceiling and, when paginated, that the reported limit equals the clamped value.paginate omitted and with default set but max omitted to record actual behavior for your versions.$limitClamping $limit is not a security boundary by itself. Also bound $skip, $sort, and broad filters with query validation or allowlists, and pair the hook with authentication and database resource limits. After any core or adapter upgrade, re-run the contract tests because query translation and default pagination can change.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.