Repository navigation
Description
TL;DR
Backfill the entire pubnet history with the production backfill path, on minimum hardware, and publish two numbers partners need to size an RPC v2 deployment:
- Time — wall-clock to backfill full history on minimum hardware (a conservative upper bound).
- Disk — total on-disk footprint, broken down by data type (ledgers vs transaction hashes vs events), as absolute size, as a percentage split, and as per-chunk unit costs partners can extrapolate from.
This is a real-data run. It reads live pubnet ledgers from a public datastore and drives the daemon's own backfill. It does not use synthetic datasets.
Why
Partners must provision hardware and storage before they run RPC v2 with full history. Two questions block that decision: how long does the initial backfill take, and how much disk does full history consume. The compressed input lake from genesis to tip is approximately 4.9 TB (measured 2026-08-10 by listing the public S3 lake). The cold-store footprint by data type is not yet measured. This run measures both and gives partners an extrapolation rule for shorter retention windows.
What "full history" means here
Full history is organized in chunks of exactly 10,000 ledgers. chunk_id = (ledger_seq − 2) / 10_000; the chain starts at ledger 2.
- Start chunk = the genesis chunk = chunk 0 (ledgers 2–10,001).
- Number of chunks = every complete chunk up to the tip. At the current tip (63,890,102 ledgers measured 2026-08-10), that is 6,389 complete chunks (chunk 0 through 6,388). Compute the exact count from the live tip on run day:
floor((tip_ledger − 1) / 10_000). The final in-flight chunk is incomplete and is not a backfill target; it is the live/hot chunk. - Chunk growth — approximately 1.7 chunks per day at the current ledger close rate.
History is front-light and back-heavy: early ledgers are nearly empty, recent Soroban ledgers are large and carry almost all events. Per-chunk time and per-chunk size both rise across the range. The whole-range run is the only faithful total; any early-chunk sample underestimates both cost and event share.
Method
Use stellar-rpc bench-ingest cold. Cold is the backfill path — it runs one RunBackfill over the chunk range: per-chunk freezes (ledger packs, txhash bins, events cold segments), the cross-window txhash index builds, and the shared worker pool. Backfill full history in a single run so the cross-window index builds (one per 1,000-chunk window, approximately 6–7 windows over full history) are included in the measured total. A chunk-by-chunk loop would not exercise them.
Read ledgers from the datastore, not from local packs, so the run reflects a real backfill:
- Source: the public S3 lake
s3://aws-public-blockchain/v1.1/stellar/ledgers/pubnet(regionus-east-2, anonymous-read — no credentials). Total size approximately 4.9 TB compressed. - Run the box in
us-east-2so datastore reads stay in-region. This benchmark measures local ingest throughput and disk cost, not cross-region download latency. An out-of-region box measures the wrong thing. - Alternative source: SDF's GCS lake (
--datastore-type=GCS,--bucket-path=sdf-ledger-close-meta/v1/ledgers/pubnet). It needs auth. Match the box region to the bucket.
Backfill streams ledgers via BufferedStorageBackend with an in-memory prefetch buffer. No local staging of the lake occurs. Disk holds the cold-store output only.
Illustrative command (confirm exact flags against the harness help):
stellar-rpc bench-ingest cold \
--source=bsb --datastore-type=S3 --region=us-east-2 \
--bucket-path=aws-public-blockchain/v1.1/stellar/ledgers/pubnet \
--start-chunk=0 --num-chunks=6389 --workers=8 \
--cold-out-dir=/mnt/data/cold --catalog-dir=/mnt/data/cold \
--out=/mnt/data/bench-out
Put --cold-out-dir and --catalog-dir on the disk being measured. --out (CSV report) can be anywhere.
Default settings
Run RPC v2 as a partner would out of the box. Change only what the box dictates.
- Workers = number of cores. The daemon defaults backfill concurrency to the CPU count (
[backfill] workers, defaults toruntime.NumCPU()). The bench flag--workersdefaults to 1, so set it explicitly to the box's core count to match the production default (--workers=8on an 8-core box). - Leave datastore prefetch and retry knobs at their defaults:
--bsb-buffer-size=0,--bsb-num-workers=0,--retry-limit=0all mean "backfill's own default." Do not tune them.
Hardware
Target minimum compute so the time is a conservative floor a small partner deployment can match: an x86 box at 8 vCPU / 32 GiB (m6id.2xlarge class). Workers follow the core count (8).
The constraint is disk. The expected cold-store footprint is 4–8 TB: the compressed input lake alone is 4.9 TB, and the cold store re-encodes the ledgers plus adds the txhash and events structures; the exact ratio between input and cold store is not yet measured. Before full provisioning, run a calibration step: ingest approximately 4 sample chunks spread across history (e.g., chunk 0, 300, 3,000, 6,345) on any small box. After each run, measure per-type sizes:
du -sh /mnt/data/cold/ledgers /mnt/data/cold/txhash /mnt/data/cold/events /mnt/data/cold/catalog
Integrate the sample bytes/chunk across the chunk range to fix the full-history footprint before provisioning. Single-chunk runs fit in 474 GB instance store; region does not affect byte counts.
Two hardware configurations to choose from:
- Recommended — storage-optimized NVMe instance, workers pinned to 8. After the calibration step, if footprint ≤ 7 TB, use an i4i.8xlarge-class instance (2×3.75 TB NVMe RAID0) and set
--workers=8to hold the compute profile at minimum. Instance-store NVMe keeps disk I/O honest, so the wall time is representative of real deployments using local fast storage. Trade-off: the box has more vCPU than the compute target, used only to support the attached NVMe. - Alternative — 8-vCPU box + EBS data volume, exact-minimum compute. Keep the m6id.2xlarge and mount a provisioned-throughput volume (gp3 tuned high, or io2) sized after the calibration step for
--cold-out-dirand--catalog-dir. This holds compute exactly at minimum. Trade-off: cold writes hit EBS, so provision throughput/IOPS appropriately — a throttled volume inflates the wall time. Report the exact volume specification (size, throughput, IOPS) in the results so the timing is interpretable.
The report must state which disk backed the run.
Wall-time estimate
Cold ingest is events-bound. A July 2026 bench campaign on m6id.2xlarge measured four real-pubnet chunks at 6.2 s (chunk 3000), 45.9 s (chunk 5000), 40.2 s (chunk 6100), and 61.7 s (chunk 6345), totaling 25.43 million events in 154.1 seconds. This yields a throughput of approximately 165,000 events/s. The model reproduces each measured chunk within 8% error.
Pubnet history holds approximately 20 billion events (calculated from measured events-per-ledger at four points across history). At 165,000 events/s, full-history backfill takes approximately 33 hours on 8-core minimum hardware. Download is not the constraint: 4.9 TB over 33 h ≈ 41 MB/s average, trivial in-region.
Plan for 1.5–3 days wall time. Uncertainty sources: (1) the July runs' worker setting is not recorded — if it was below 8, the real 8-worker run finishes faster than the estimate. (2) The events curve has limited sampling in the 30–50M ledger range where activity spikes; real history may carry more events. (3) Measured chunks exclude the approximately 6–7 cross-window txhash index rebuilds — unmeasured cost, add allowance. (4) Recent improvements to chunk-build performance shorten the wall time if merged before the run.
Measurements and report
Time. From driver.csv:
backfill_wall— total backfill wall time. This is the headline number.ledgers_total,txhash_total,events_total— per-type ingest time (which data type dominates the clock).index_rebuild— cross-window txhash index build cost.peak_rss_bytes— peak process memory (Linux only), so partners can confirm 32 GiB is enough.
Disk. After the run, size each data-type subtree under --cold-out-dir:
du -sh /mnt/data/cold/ledgers /mnt/data/cold/txhash /mnt/data/cold/events /mnt/data/cold/catalog
Report:
- Absolute size per subtree (
ledgers/,txhash/,events/,catalog/) and the total. - The percentage split across ledgers / transaction hashes / events.
- Per-unit costs for extrapolation: bytes per chunk, bytes per ledger, bytes per million events.
- An extrapolation rule so a partner can size a shorter retention window from the full-history total.
State the box model, region, disk backing and spec (if EBS, include size/throughput/IOPS), exact --num-chunks and tip ledger, and total wall time alongside the numbers.
Risks
- Long single run. Full history is a 1.5–3 day run. Backfill uses a fresh scratch catalog per run and does not resume, so a mid-run failure loses progress. Run under tmux/screen, monitor free disk, and treat one clean pass as the deliverable.
- Calibration first. Before committing the full run, do the short calibration step above to validate the command, box, and disk-write rate and to produce the exact disk bracket. Measure both early and recent chunks — early-only samples underestimate event volume and final-chunk size.
- Final chunks largest. The tail of history carries the most events and largest ledgers per chunk. Monitor free space in the final half of the run.
Done when
A full-history backfill has run to completion on minimum hardware, and a published record reports the total backfill time, the peak memory, and the total disk footprint with a per-data-type percentage split and per-unit extrapolation constants. The record states the box model, region, disk backing (and volume spec if EBS), the exact chunk range run, and the corresponding tip ledger.
Related
- Full-history benchmark commands — ingest across all data types #771 — the
bench-ingest coldcommand this run uses. - Benchmarking infra: stellar-rpc-bench S3 bucket + benchmarking devbox #866 — results bucket for the CSVs and the report (GCS fallback OK).
- e2e benchmark campaigns workflow — dispatch, run on EC2, publish results #868 — campaign automation. This run is manual (single very long run); automation is optional here.
- Independent of Synthetic ledger datasets for Phases 1–3 ("golden packs") #867 — reads real pubnet from the public lake, not synthetic golden packs.
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsDone
Activity
marwen-abid commentedon Aug 17, 2026
Results — full-history backfill on minimum hardware
Full pubnet history is backfilled. Wall time 18 h 44 m, cold store 6.77 TB, and the memory answer the issue asked for: 32 GiB is not enough.
Setup
feature/full-history, commitd9408b45.bench-ingest cold --source=bsb,--workers=8(the production default of one per core).aws-public-blockchain, us-east-2, anonymous read). Cross-region from the box; the measured 16 s/chunk download floor is included in the wall time.Numbers
The issue's ~2 TB provisioning guidance is superseded: provision ≥8 TB for full history plus a year of growth.
Method, and why we iterated
The first attempt ran the specified single pass over all chunks. The kernel OOM-killed it at 30.4 GiB anon-RSS after 4 h (~chunk 3,976): cross-window txhash index builds run concurrently with chunk builds on the shared worker pool, each in-flight chunk holds a 5,000-ledger download prefetch buffer, and the bench CSV sink accumulates for the whole run.
The completed run split the work into 7 back-to-back invocations, one per 1,000-chunk txhash index window, with a 64 GiB swap file as a safety net. This keeps every window index complete (an invocation only indexes its own range), bounds each process to one index build, and includes each index build in its segment's wall time. The total is the sum of the 7 segments with zero gaps. Even segmented, every heavy window peaked near 29 GB RSS and pushed up to 8.3 GB into swap — the swap file was load-bearing, not a spike absorber.
Memory finding — applies to production
At
d9408b45, daemon startup runs backfill-to-tip before hot ingestion starts (never concurrently), production backfill is windowed, and the CSV sink is bench-only — but the two dominant memory drivers ship in production at identical defaults ([backfill.bsb].buffer_size= 5000,num_workers= 50, index builds at NumCPU/2). A 32 GiB box will hit this wall during initial sync. The reducing knobs exist ([backfill].workers,[backfill.bsb].buffer_size/num_workers) but are undocumented, and no GOMEMLIMIT is set. Suggested follow-ups: bound backfill memory (GOMEMLIMIT + a cap on concurrent index builds), and document RAM sizing — until then, recommend 64 GiB for full-history backfill.Raw data: per-window driver/store CSVs,
duoutput, and the 1-minute memory timeline are bundled; a visual report with the full charts is available on request.marwen-abid commentedon Aug 17, 2026
More Info + Graphs: https://claude.ai/code/artifact/b500896c-feb1-4e15-97a4-2b1db871e943?via=auto_preview
marwen-abid commentedon Aug 18, 2026
Follow-up: full-history rerun on
tamirms/full-history-p99-campaignWe reran the identical benchmark on the same box (m6id.2xlarge, 32 GiB, 10 TB gp3) at
24c6e613, as a single pass withGOMEMLIMIT=26GiB— the configuration that OOM'd ond9408b45.d9408b4524c6e613Effective concurrency was 7.73× on 8 workers; the 2 h 14 m of cross-window index-build time was fully hidden behind chunk builds.
Conclusion: the branch's memory work (events-index spill-and-merge, streaming CSV sink, packed-row txhash) converts the run from "requires segmentation + a swap file on 32 GiB" to "one clean unattended pass" at equal wall time. The workload is compute-bound at ~7.7/8 cores on both refs, so the per-chunk speedup (−7% on the heaviest chunk) is absorbed at full scale; the win is feasibility on minimum hardware, which resolves the memory finding from the previous run.
GOMEMLIMITis still doing real work — RSS rode the 26 GiB ceiling through every heavy window — so we recommend setting it (or an equivalent bound) by default for backfill on 32 GiB boxes. .6 remaining items
backfill: bound prefetch memory with pool-wide budgets
backfill: bound prefetch memory with pool-wide budgets
backfill: bound prefetch memory with pool-wide budgets
backfill: bound prefetch memory with pool-wide budgets
backfill: bound prefetch memory with pool-wide budgets
backfill: bound prefetch memory with pool-wide budgets
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task
backfill: cap prefetch memory per chunk task