Home / Blockfrost.io / blockfrost-backend-ryo
May 12, 6-7 PM (0)
May 12, 7-8 PM (0)
May 12, 8-9 PM (0)
May 12, 9-10 PM (0)
May 12, 10-11 PM (0)
May 12, 11-12 AM (0)
May 13, 12-1 AM (0)
May 13, 1-2 AM (0)
May 13, 2-3 AM (0)
May 13, 3-4 AM (0)
May 13, 4-5 AM (0)
May 13, 5-6 AM (0)
May 13, 6-7 AM (0)
May 13, 7-8 AM (0)
May 13, 8-9 AM (0)
May 13, 9-10 AM (0)
May 13, 10-11 AM (0)
May 13, 11-12 PM (0)
May 13, 12-1 PM (0)
May 13, 1-2 PM (0)
May 13, 2-3 PM (0)
May 13, 3-4 PM (0)
May 13, 4-5 PM (0)
May 13, 5-6 PM (0)
May 13, 6-7 PM (0)
May 13, 7-8 PM (0)
May 13, 8-9 PM (0)
May 13, 9-10 PM (0)
May 13, 10-11 PM (0)
May 13, 11-12 AM (0)
May 14, 12-1 AM (0)
May 14, 1-2 AM (0)
May 14, 2-3 AM (0)
May 14, 3-4 AM (0)
May 14, 4-5 AM (0)
May 14, 5-6 AM (0)
May 14, 6-7 AM (0)
May 14, 7-8 AM (0)
May 14, 8-9 AM (0)
May 14, 9-10 AM (0)
May 14, 10-11 AM (0)
May 14, 11-12 PM (0)
May 14, 12-1 PM (0)
May 14, 1-2 PM (0)
May 14, 2-3 PM (1)
May 14, 3-4 PM (3)
May 14, 4-5 PM (3)
May 14, 5-6 PM (0)
May 14, 6-7 PM (0)
May 14, 7-8 PM (0)
May 14, 8-9 PM (0)
May 14, 9-10 PM (0)
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)
15 commits this week May 12, 2026 - May 19, 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]>
perf: push pagination before joins in drep-votes and delegations SQL
Wrap the driving-table filter + ORDER BY + LIMIT/OFFSET in a CTE so
Postgres materializes the page first, then joins to tx / block /
gov_action_proposal / pool_hash only for the page we return — not
for every matching row.

Two endpoints applied (paged + unpaged variants each):

  GET /governance/dreps/:drep_id/votes
    Original plan: seq-scan voting_procedure (25k rows on mainnet),
    hash-join to drep_hash → 130 rows for a busy DRep, 4 nested-loop
    joins fire on all 130, sort + top-N 100. ~65ms warm.
    New plan: filter voting_procedure to the drep's 130 rows, sort
    + limit to 100, then 3 nested-loop joins fire only on 100. ~30ms warm.
    Live end-to-end: ~140ms → ~50ms (~2.9x).

  GET /accounts/:stake_address/delegations
    Original plan: stake_address → delegation index scan returns 1282
    rows for a heavily-delegated account, 3 joins (tx + block + pool_hash)
    fire on all 1282, then sort + top-N 100. ~60ms warm SQL.
    New plan: filter + sort + limit delegations to 100 (via d.tx_id
    ordering directly from the delegation row), then 3 joins fire on
    100. ~2ms warm SQL. 27x speedup at the DB layer.

No response shape changes. Same fields, same ordering semantics, same
edge cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
perf: push pagination before joins in committee votes SQL
Wrap the voting_procedure filter + ORDER BY + LIMIT/OFFSET in a
`paged_votes` CTE so Postgres materializes the page first (via an
index scan on voting_procedure_pkey), then does the joins against
tx, block, gov_action_proposal, tx (proposal), and voting_anchor
only for the page we return — not for every CC vote on the chain.

Live end-to-end measurements (mainnet dev backend):

  /committee/votes?count=100             170ms → 50ms   (3.4x)
  /committee/votes?count=100&order=desc  190ms → 50ms   (3.8x)
  /committee/:cc_hot/votes?count=100     150ms → 50ms   (3.0x)
  /committee/:cc_cold/votes?count=100    145ms → 47ms   (3.1x)

EXPLAIN buffer reads dropped from ~12k to ~1.9k for count=100, which
also reduces cold-cache tail latency.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
fix: filter committee selection to enacted NewCommittee actions
db-sync inserts a committee row for every NewCommittee proposal,
not only enacted ones. The previous ORDER BY committee.id DESC LIMIT 1
returned the most recently proposed committee even when it had
expired or been dropped, surfacing the wrong gov_action_id on
/governance/committee.

Fix: LEFT JOIN to gov_action_proposal and keep only rows where the
proposal is enacted (or the genesis row, which has no proposal).
Order by enacted_epoch DESC with id as tiebreaker.

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