Answer
For Redis 7.0+ with a requirement for strict score order in user-facing pages, do not use ZSCAN for pagination. Use ZRANGE with LIMIT for shallow pages and keyset seek pagination with score+member as a cursor for deep pages. There is no documented way to make ZSCAN return score-ordered results, so cursor stability from ZSCAN cannot be combined with rank-based precision.
Confirmed facts
- ZRANGE returns members in score order by rank range. It supports LIMIT offset count and WITHSCORES and gives deterministic ordering for a given key snapshot.
- ZSCAN is a generic incremental cursor scan over the internal dictionary encoding. It iterates by internal cursor, not by sorted set score order, and does not guarantee score ordering or stable pagination across calls.
- ZRANGE with a large offset requires Redis to traverse the skiplist to the offset before returning the window. Cost is O(log N + M) with offset traversal growing with depth.
- ZSCAN work per call is roughly constant for a fixed COUNT hint, but order is not score order.
Likely explanation and recommendation
ZSCAN is intended for full key iteration or maintenance tasks, not for ordered page views. Client-side re-sorting of ZSCAN output defeats the purpose of a sorted set and increases client memory.
For stable, ordered pagination without deep offset cost, use a seek cursor based on last seen score and member:
- First page: ZRANGE key 0 pageSize-1 WITHSCORES
- Remember last member and its score from the page.
- Next page: query from the next logical position using score and member ordering.
In practice this is ZRANGEBYSCORE with an exclusive lower bound on score, and a tie-breaker on member within the same score. The pattern avoids offset traversal and remains stable under inserts between requests, at the cost of losing random jump to page N.
# first page
ZRANGE myzset 0 99 WITHSCORES
# seek next page after last_score, last_member
ZRANGEBYSCORE myzset (last_score +inf LIMIT 0 100
If ties on score are possible, use the member as a secondary cursor: score = last_score and member > last_member. This requires application logic to build the min bound.
Assumptions and uncertainty
This recommendation assumes read-mostly access and that users can accept next/prev navigation instead of arbitrary page numbers. Write churn during a session can still cause items to appear on two pages or be skipped if scores change. Redis does not provide a built-in transactional snapshot for pagination.
Verification you can run in a test key:
- Run ZRANGE with increasing offset and observe latency growth.
- Run ZSCAN cursor 0 COUNT 100 and inspect returned order to confirm it is not score ordered.
- Compare ZRANGE ... LIMIT offset count versus repeated ZRANGEBYSCORE seek under inserts.
One missing diagnostic that changes the recommendation: what is the maximum page offset you must support and the write rate to the sorted set during a user session. If you need random access to page 10,000+ with high write churn, a different external index is required.