Home / Blockfrost.io / blockfrost-backend-ryo
May 14, 10-11 PM (0)
May 14, 11-12 AM (0)
May 15, 12-1 AM (0)
May 15, 1-2 AM (0)
May 15, 2-3 AM (0)
May 15, 3-4 AM (0)
May 15, 4-5 AM (0)
May 15, 5-6 AM (0)
May 15, 6-7 AM (0)
May 15, 7-8 AM (0)
May 15, 8-9 AM (4)
May 15, 9-10 AM (0)
May 15, 10-11 AM (0)
May 15, 11-12 PM (0)
May 15, 12-1 PM (0)
May 15, 1-2 PM (2)
May 15, 2-3 PM (0)
May 15, 3-4 PM (0)
May 15, 4-5 PM (0)
May 15, 5-6 PM (0)
May 15, 6-7 PM (0)
May 15, 7-8 PM (0)
May 15, 8-9 PM (0)
May 15, 9-10 PM (0)
May 15, 10-11 PM (0)
May 15, 11-12 AM (0)
May 16, 12-1 AM (0)
May 16, 1-2 AM (0)
May 16, 2-3 AM (0)
May 16, 3-4 AM (0)
May 16, 4-5 AM (0)
May 16, 5-6 AM (0)
May 16, 6-7 AM (0)
May 16, 7-8 AM (0)
May 16, 8-9 AM (0)
May 16, 9-10 AM (0)
May 16, 10-11 AM (0)
May 16, 11-12 PM (0)
May 16, 12-1 PM (0)
May 16, 1-2 PM (0)
May 16, 2-3 PM (0)
May 16, 3-4 PM (0)
May 16, 4-5 PM (0)
May 16, 5-6 PM (0)
May 16, 6-7 PM (0)
May 16, 7-8 PM (0)
May 16, 8-9 PM (0)
May 16, 9-10 PM (0)
May 16, 10-11 PM (0)
May 16, 11-12 AM (0)
May 17, 12-1 AM (0)
May 17, 1-2 AM (0)
May 17, 2-3 AM (0)
May 17, 3-4 AM (0)
May 17, 4-5 AM (0)
May 17, 5-6 AM (0)
May 17, 6-7 AM (0)
May 17, 7-8 AM (0)
May 17, 8-9 AM (0)
May 17, 9-10 AM (0)
May 17, 10-11 AM (0)
May 17, 11-12 PM (0)
May 17, 12-1 PM (0)
May 17, 1-2 PM (0)
May 17, 2-3 PM (0)
May 17, 3-4 PM (0)
May 17, 4-5 PM (0)
May 17, 5-6 PM (0)
May 17, 6-7 PM (0)
May 17, 7-8 PM (0)
May 17, 8-9 PM (0)
May 17, 9-10 PM (0)
May 17, 10-11 PM (0)
May 17, 11-12 AM (0)
May 18, 12-1 AM (0)
May 18, 1-2 AM (0)
May 18, 2-3 AM (0)
May 18, 3-4 AM (0)
May 18, 4-5 AM (0)
May 18, 5-6 AM (0)
May 18, 6-7 AM (0)
May 18, 7-8 AM (0)
May 18, 8-9 AM (0)
May 18, 9-10 AM (0)
May 18, 10-11 AM (0)
May 18, 11-12 PM (0)
May 18, 12-1 PM (0)
May 18, 1-2 PM (0)
May 18, 2-3 PM (0)
May 18, 3-4 PM (0)
May 18, 4-5 PM (0)
May 18, 5-6 PM (0)
May 18, 6-7 PM (0)
May 18, 7-8 PM (0)
May 18, 8-9 PM (0)
May 18, 9-10 PM (0)
May 18, 10-11 PM (0)
May 18, 11-12 AM (0)
May 19, 12-1 AM (0)
May 19, 1-2 AM (0)
May 19, 2-3 AM (0)
May 19, 3-4 AM (0)
May 19, 4-5 AM (0)
May 19, 5-6 AM (0)
May 19, 6-7 AM (1)
May 19, 7-8 AM (0)
May 19, 8-9 AM (0)
May 19, 9-10 AM (0)
May 19, 10-11 AM (0)
May 19, 11-12 PM (0)
May 19, 12-1 PM (0)
May 19, 1-2 PM (0)
May 19, 2-3 PM (0)
May 19, 3-4 PM (0)
May 19, 4-5 PM (0)
May 19, 5-6 PM (0)
May 19, 6-7 PM (1)
May 19, 7-8 PM (0)
May 19, 8-9 PM (0)
May 19, 9-10 PM (0)
May 19, 10-11 PM (0)
May 19, 11-12 AM (0)
May 20, 12-1 AM (0)
May 20, 1-2 AM (0)
May 20, 2-3 AM (0)
May 20, 3-4 AM (0)
May 20, 4-5 AM (0)
May 20, 5-6 AM (0)
May 20, 6-7 AM (0)
May 20, 7-8 AM (0)
May 20, 8-9 AM (0)
May 20, 9-10 AM (0)
May 20, 10-11 AM (0)
May 20, 11-12 PM (0)
May 20, 12-1 PM (0)
May 20, 1-2 PM (0)
May 20, 2-3 PM (0)
May 20, 3-4 PM (0)
May 20, 4-5 PM (0)
May 20, 5-6 PM (0)
May 20, 6-7 PM (0)
May 20, 7-8 PM (0)
May 20, 8-9 PM (0)
May 20, 9-10 PM (0)
May 20, 10-11 PM (0)
May 20, 11-12 AM (0)
May 21, 12-1 AM (0)
May 21, 1-2 AM (0)
May 21, 2-3 AM (0)
May 21, 3-4 AM (0)
May 21, 4-5 AM (0)
May 21, 5-6 AM (0)
May 21, 6-7 AM (0)
May 21, 7-8 AM (0)
May 21, 8-9 AM (0)
May 21, 9-10 AM (0)
May 21, 10-11 AM (0)
May 21, 11-12 PM (0)
May 21, 12-1 PM (0)
May 21, 1-2 PM (0)
May 21, 2-3 PM (0)
May 21, 3-4 PM (0)
May 21, 4-5 PM (0)
May 21, 5-6 PM (0)
May 21, 6-7 PM (0)
May 21, 7-8 PM (0)
May 21, 8-9 PM (0)
May 21, 9-10 PM (0)
May 21, 10-11 PM (0)
8 commits this week May 14, 2026 - May 21, 2026
perf: switch to stake_address.hash_raw ordering for stable + fast pagination
The previous fix on this branch ordered by the slot_no of the earliest
delegation per (addr, pool). That ordering was correct and chain-stable
but had a fatal perf characteristic: it forced a correlated subquery on
`delegation` for every row in `epoch_stake` (~1.3M rows per recent epoch
on mainnet). Page 1 of /epochs/:number/stakes took ~5s in EXPLAIN ANALYZE
even though the page itself is only 100 rows — top-N sort still requires
the sort key for every input row.

We tried several escape hatches before giving up on seniority ordering:

  - LATERAL subquery with (slot_no, tx_id, cert_index) tie-break to remove
    the replica-local addr_id fallback. Correctness improved, perf
    unchanged: still ~5s, same loop count.

  - 225MB covering index on
    delegation(addr_id, pool_hash_id, active_epoch_no, slot_no, tx_id, cert_index)
    Went to 6.9s — the index turned each lookup into an Index Only Scan
    but the range filter on active_epoch_no (column 3) meant rows still
    came back ordered by active_epoch_no, not by slot_no, so the planner
    re-sorted inside every loop. Reordering to put slot_no before
    active_epoch_no would have helped but only to ~1-2s, still far from
    what the original es.id query (~17ms) delivered.

  - Walking delegation backwards (latest-first) to invert the LATERAL.
    Doesn't work because a stake address can re-delegate to a different
    pool — only the most recent delegation matches epoch_stake, so any
    earlier delegations we see during the walk are noise.

The only paths to sub-second seniority ordering are materializing the
first-deleg-per-(addr, pool) into a helper table (Koios's approach via
grest.stake_distribution_cache) or upstreaming it into db-sync. Both are
large engineering changes for a query traffic of ~2k calls/day where the
common case is already <50ms via HTTP-layer caching.

Instead, drop seniority semantics and order by stake_address.hash_raw —
chain-derived, deterministic across replicas, and lets the planner walk
the existing `unique_stake_address` index, probing epoch_stake via
`unique_epoch_stake` (epoch_no, addr_id) with early-exit on LIMIT.
EXPLAIN ANALYZE on epoch 500 (1.3M rows):

  /epochs/:number/stakes page 1:           5000ms → 138ms

For /epochs/:number/stakes/:pool_id the win is smaller (7s → 3s on the
biggest pool) because that endpoint's bottleneck is the BitmapAnd
finding pool delegators for the epoch, not the sort. A future
bf_idx_epoch_stake_pool_epoch(pool_id, epoch_no) would close that gap
but isn't urgent at current traffic.

Both endpoints now sort by sa.hash_raw so pagination order is consistent
between them. Returned bech32 in `stake_address` is rendered from
sa.view; the binary hash_raw is only the sort key.

Trade-off: offset pagination cost grows linearly with page depth on
/epochs/:number/stakes (page 1 ≈ 138ms, page 10 ≈ 1s, page 100 ≈ 5-10s).
This is the standard "walk N matches, discard" cost of OFFSET on an
index-walk plan; the only escape would be cursor pagination, which would
change the public API. Acceptable for current usage.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
fix: stable ordering for /epochs/:number/stakes across db-sync replicas
The previous SQL paginated by epoch_stake.id (an auto-increment assigned
at insertion time by db-sync). Two replicas with identical chain data
can have different epoch_stake.id ranges for the same epoch — db-sync
recomputes the snapshot per epoch, and rollback/replay can leave gaps
or reorder insertions. Result: page 1 returned a different subset of
stake_address rows on each backend.

Verified on two backends with bit-identical data (md5 of the full set
matched) but different id ranges (91359..92937 vs 93823..95434 for the
same epoch 121, 1579 rows each).

Fix: order by the slot_no of the `delegation` row that established the
(addr, pool) pairing effective in this epoch. slot_no is pure chain
data, stable across any replica. Tiebreak on addr_id.

Applied to:
- /epochs/:number/stakes              (paged + unpaged)
- /epochs/:number/stakes/:pool_id     (paged + unpaged)

Same response shape, same set of rows — just deterministic ordering.

Plan cost: a correlated subquery on `delegation` fires once per
epoch_stake row, served by `idx_delegation_addr_id`. ~7 ms total on
preview's epoch 121 (1579 rows), unchanged from the previous 6.5 ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
perf: index off_chain_vote_fetch_error (voting_anchor_id, id)
The metadata endpoints (/governance/proposals/:gov_action_id/metadata
and /governance/dreps/:drep_id/metadata) LATERAL-join a `LIMIT 1` lookup
on the latest fetch_error for the anchor:

  SELECT fetch_error FROM off_chain_vote_fetch_error
  WHERE voting_anchor_id = va.id
  ORDER BY id DESC LIMIT 1

Without an index on (voting_anchor_id, id), Postgres picks
`Index Scan Backward off_chain_vote_fetch_error_pkey` and applies
voting_anchor_id as a filter — fine when matches are near the top
of the id range, catastrophic when matches are few/zero. Measured
on a preview db-sync with 1.19M fetch_error rows and a proposal
whose anchor has zero matches: 187ms, scanning the entire table
backwards just to prove a negative.

After the index, the planner can satisfy both the equality filter
and the ORDER BY id DESC LIMIT 1 from the same btree — sub-ms
regardless of how many fetch_error rows exist or where they are.

Documentation-only change in the repo (README index list + CHANGELOG).
Operators need to create the new index manually, same convention as
the existing `bf_idx_*` indexes in README.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>