Kline pagination done right: cursors, not offsets
Every charting UI eventually hits the same problem: the user drags the chart left, past the candles you've loaded, and you need the next page of history. How you fetch that page decides whether your chart is stable or subtly broken.
The offset trap
The obvious design is ?offset=500&limit=500 — skip what you have, take the next batch. On a static dataset that works. On a live market it doesn't, because the dataset moves underneath the offset. Between your first request and your second, new candles close and get appended at the head of the series. Offset 500 no longer points where it pointed a moment ago; your second page overlaps the first or skips a candle entirely. Users see duplicated bars or holes, usually right at page boundaries, usually impossible to reproduce in a bug report.
Cursors anchor to data, not position
CryHub's kline endpoint paginates with a cursor instead. Every response envelope carries next_cursor — the open timestamp of the oldest candle in the page you just received:
{
"success": true,
"ts": 1777804869193,
"next_cursor": 1777651200000,
"data": [ ... ]
}
To fetch the previous page, pass that value back as end_time:
GET /rest/v1/market/kline?source=s1&symbols=btc"e=usdt
&slice=60min&size=500&end_time=1777651200000
Because the cursor is a timestamp, not a position, it is immune to new candles arriving at the head. The page it selects is defined by the data itself. You can walk backward through years of history while the market ticks forward, and the pages tile perfectly.
Client-side rules that make it robust
Our own widgets follow four rules, and we'd suggest the same for any client:
- Key pages by
(pair, timeframe)and reset the cursor whenever either changes — a cursor from BTC/1h means nothing on ETH/15m. - Track
exhausted. When a page comes back shorter than requested, mark the series complete and stop asking. - Guard against double-fires. Chart scroll events fire in bursts; if a request is already in flight for a key, drop the trigger.
- Cap the buffer. We keep at most 5,000 candles in memory per series — ten pages — and let the oldest fall away. Nobody inspects candle 12,000 at 1-minute resolution; they switch to a higher timeframe.
The subtle part: preserving the view
The trickiest bit isn't fetching — it's what happens after. Prepending 500 candles shifts every logical index in the chart by 500, and a naive implementation makes the viewport jump. The fix is bookkeeping: record the visible logical range before the prepend, re-apply it shifted by the prepend count after. The user never notices anything except that history keeps existing wherever they scroll.
Offset pagination is fine for blog archives. For market data, anchor to the data.