hyperdxio / hyperdxio/hyperdx

Non-deterministic pagination in /api/v2/search when rows share the same timestamp

Open
#2,614 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

external
Dominant language
TypeScript
Stars
9.9k
Forks
471
Avg merge
2d 4h
Merged PRs (30d)
117

Description

Problem

The POST /api/v2/search endpoint (added in [hyperdxio/hyperdx#2258](https://github.com/hyperdxio/hyperdx/pull/2258)) uses LIMIT/OFFSET pagination with results ordered by Timestamp. When multiple rows share the same timestamp, pagination becomes non-deterministic -- rows near a page boundary can be duplicated or skipped across pages.

Example: If rows 90-110 all have the same Timestamp value and the client requests maxResults=100, offset=0 followed by maxResults=100, offset=100, ClickHouse arbitrarily assigns which rows fall on which page. The client may see some rows twice and miss others entirely.

Why this happens in practice

While Timestamp is DateTime64(9) (nanosecond precision), duplicate timestamps are common in real-world OTEL pipelines:

  • Collector-assigned timestamps: When logs arrive at the OTEL collector without a timestamp, the collector assigns one. An entire batch receives the same timestamp.
  • Limited source precision: Many language runtimes only produce microsecond or millisecond precision. The remaining digits are zero-padded, reducing the effective keyspace.
  • High-throughput services: Multiple log lines from the same flush, HTTP request, or function call naturally share the same wall-clock time.

Adding search predicates reduces the likelihood (fewer matching rows means fewer ties), but the issue is still possible in any high-volume deployment.

Proposed solution

For tables with enable_block_number_column = 1 and enable_block_offset_column = 1 (which ClickStack already enables on otel_logs), add _block_number and _block_offset as tiebreaker columns in the ORDER BY clause:

ORDER BY Timestamp DESC, _block_number DESC, _block_offset DESC
LIMIT {maxResults} OFFSET {offset}

This produces a deterministic total ordering since (_block_number, _block_offset) uniquely identifies each row within a part. The additional columns should have negligible performance impact when using read_in_order.

Alternatives considered
  • Cursor-based pagination (filter by WHERE Timestamp < :last_seen): Avoids offset entirely but still non-deterministic when ties exist at the cursor boundary. Would also require a tiebreaker.
  • Nanosecond precision alone: Reduces probability but does not eliminate the issue, especially for collector-assigned timestamps.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the implementation of the POST /api/v2/search endpoint and trace the ClickHouse query that applies Timestamp ordering and LIMIT/OFFSET pagination. Verify the relevant table settings for _block_number and _block_offset, then check the endpoint's existing tests or add coverage for equal timestamps. Done means consecutive pages have deterministic ordering without skipped or duplicated rows.

Written by the indexing model from the issue text.

Assessment

Tech stack
clickhouse, typescript
Domain
api, backend, databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.