Pagination and rate limits
A collection endpoint that returns everything is fine until the collection is large, and a client that asks politely is fine until there are ten thousand of them. Both problems appear only at scale, and both come down to the same thing: the server and the client disagree about what time it is.
Offset pagination is correct arithmetic on the wrong list
?offset=5&limit=5 means "skip five, take five", and it is exactly right — over a list
that has not changed. On a newest-first feed, one row inserted at the front while the
client is between pages shifts everything down by one, so item five arrives twice. One
row deleted shifts everything up, and an item the client never saw is skipped entirely.
A cursor names a position in the ordering rather than a count from the start, so edits elsewhere in the list cannot move it:
| Offset | Cursor | |
|---|---|---|
| Stable under edits | No | Yes |
| Jump to page 50 | Yes | No |
| Show "page 3 of 27" | Yes | Not usually |
| Cost on a large table | Grows with offset | Constant |
The Link header carries the next cursor, so the client follows a URL rather than
constructing one — which means the scheme can change without breaking every caller.
Rate limits: a level and a timestamp
A rate limit is usually a token bucket, and the implementation is smaller than people expect: no timers, no background jobs, no queue. Just a level and the time it was last updated, recomputed whenever a request arrives.
Capacity and refill rate are different numbers doing different jobs: capacity is how big a burst you tolerate, and the refill rate is the sustained limit. A refused request costs no token, or a client hammering the endpoint could hold its own bucket permanently empty.
When the bucket is dry the answer is 429, and Retry-After turns a refusal into an
instruction. It is expressed as a delta rather than an absolute time because clocks
disagree.
One caution on the headers: 429 and Retry-After are standardised and dependable. The
RateLimit-* family is still a draft, and X-RateLimit-* is a convention with no
specification at all — so read them if they are there and never require them.
Key takeaways
- Offset pagination is correct arithmetic over a list that changed underneath it: one insert duplicates a row, one delete hides one.
- A cursor names a position in the ordering, so it survives edits — at the cost of random access and “page 3 of 27”.
- A token bucket is a level and a timestamp computed on arrival. There is no timer anywhere.
- Capacity is the burst you tolerate; the refill rate is the sustained limit. They are different numbers.
- Refused requests cost no token, so ignoring Retry-After gains nothing — and only 429 and Retry-After are actually standardised.