9 min
  • APIs
  • HTTP
  • Caching

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.

Loading the API Visualizer simulation…

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:

OffsetCursor
Stable under editsNoYes
Jump to page 50YesNo
Show "page 3 of 27"YesNot usually
Cost on a large tableGrows with offsetConstant

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.

Check yourself
A client reads page 1 of a newest-first feed, then a new item is posted, then it reads page 2. What does it see?

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.

Loading the API Visualizer simulation…

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.

Predict what happens next
A client ignores Retry-After and keeps retrying every 100 ms. What happens to it?

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.

Take it further