A cursor usually encodes the sort key of the last item on the previous page, often as a base64 blob holding a timestamp and a unique id (Mohammad Faani on cursor design). That is a sound starting point. Trouble starts when teams treat the cursor as a miniature session record and pack in filter state, sort order, tenant id, user id, permissions and a list of claims.
Three multipliers stack up. JSON repeats field names on every token: "created_at" costs twelve bytes and "2025-01-01T10:00:00Z" costs twenty, before quotes and commas. Base64 then inflates the byte string by 33 percent, and about 4 percent more if line breaks are added (Wikipedia on Base64). Signing adds a fixed 32 to 256 bytes depending on algorithm, and the signature is base64-encoded too.
Work a case. A JSON cursor at 340 bytes becomes 456 bytes after base64, and a 32-byte HMAC becomes 44 characters. The transport form is roughly 500 bytes plus the header name. Fine. Now someone adds the user's role list, the search term and the full filter object. JSON hits 1,800 bytes, base64 takes it to 2,408, the signature adds 44, and one header value is now 2,450 bytes. Send a next and a prev link together and the request carries almost 5 KB of header value before cookies, auth tokens or the user agent string.
HTTP does not police this. It defines no maximum, but Apache defaults to an 8 KB request field limit and IIS allows 16 KB, returning 413 when exceeded (Stack Overflow on header limits). Node's parser shipped with an 8 KB HTTP_MAX_HEADER_SIZE, and a long issue thread argued for 16 KB because clients were already sending large headers (Node.js issue 27645). Some commercial products expose a server.max-http-request-header-size setting and recommend starting at 8 KB, raising it only when traffic demands (Digital.ai docs). The ceiling is whatever the smallest proxy, CDN or server in your chain allows.



