CloudBitmaps

Benchmarks · aws us-east-1 on-demand · gated

What it costs, and where it stops paying.

Measured against real S3 — no emulator — with the request counts read off the AWS SDK rather than off our own arithmetic. Every money figure and rate below is checked against its source by a build gate, so a regression or an overclaim fails the build.

The cost anchors → What they do not establish

Modelled figures come from the shipped estimateCost(), measured ones from each run's committed record — neither can drift ahead of its source.

Cost anchors

Published rates applied to wire-metered operation counts. The last row is the crossover, in GETs and in cold intersects — past it a flat node is simply cheaper.

Cost anchorsestimateCost() + the AWS runs
Scenario Value Basis Verdict
1M count() calls $0.14 Zero payload reads — summed straight from the .crbm index, and exact rather than estimated. This is the object-store half of the July run: the pointer read that resolves the segment's current generation was billed to a NoSQL registry that no longer ships, so it is not in this figure. Win
1.2 GiB at rest, no traffic $0.03 /mo 0.008% of a Redis-HA node. Nothing is billed while nobody is asking. Win big
A ∩ B over two 2M-id segments 100 / 2,000 Chunks fetched in 24.6 ms — 1,900 per segment never requested. Step through it. Win big
1M cold A ∩ B on real S3 $82.40 Two 500,000-id segments sharing 100 of 1,999 chunks: 206 GETs at the median — a tail read per operand, the shared chunks, and each pointer read twice, because from a laptop an intersect outlasted the 2 s pointer refresh. Priced by the chunks the two share, not by their size, while each index fits the 256 KiB tail read. Every one cold, and every one exact. Its crossover is the last row. The single-bucket run. Win
1M loads, written and published $11.20 A generation written once, then the pointer moved to it: 2 PUT + 3 GET, measured on real S3 with the pointer in the same bucket. store.load() also lists and collects, which about doubles it. The meter counts objects, not ids — so batch a refresh. Batch it
Sustained read throughput above 329.15 /s
1.6 A ∩ B/s
Object GETs with the cache off — the pessimal posture, chosen deliberately — against a standing Redis-HA node at $346 /mo. The same line, for cold intersects of the run's shape at 206 GETs each, is 1.6 a second. What moves this line. Use a flat tier

Assumptions: cache hit rate 0, the stored-bytes term at zero, us-east-1 on-demand request rates, and the reserved-RAM baseline is an ElastiCache HA cluster — 1 primary + 2 replicas on cache.m7g.large, about $115/mo single-node. And one crossover rather than two: a segment changes only by publishing a whole new generation, so the write side is priced per object PUT and a pointer — the loads row — and a writes-per-second threshold would put a rate on an operation the library cannot perform. Change an assumption and the read line moves; four of them are how far.

Past the crossover

That row is a threshold on one axis — sustained reads — and that axis is the only one where a flat node competes, because the write side is not a rate at all. Two of the four inputs behind it are set to their worst plausible value on purpose; the other two are set in our favour, and this table says which is which. Two of them can remove the crossover entirely.

What moves this line All four are inputs, not constants
Input Published as Change it and
Cache hit rate 0 Every read billed — the pessimal value, chosen so the figure cannot flatter us. A cache that actually works moves the line out by the reciprocal of the miss rate, and at a 100% hit rate the crossover disappears: the estimator returns Infinity, because nothing reaches the meter.
Per-GET request rate on-demand The profile's storage.getPerMillion, at the published us-east-1 list rate, with no volume or committed discount applied. The line is inversely proportional to it, and a store with no per-request charge at all removes it outright: no meter, no crossing.
Stored bytes zero The first of the two set in our favour, so it is stated: storage is subtracted from the baseline before the division, and publishing it at zero hands the whole node price to the read axis as headroom. The reference corpus — $0.03 /mo, or 0.008% of the node — is how much that flatters us.
The baseline itself $346 /mo The second: three nodes rather than one, and the line scales with whatever price you compare against — a single-node baseline at about $115/mo lowers it in the same ratio. Feed estimateCost() the node you would actually run.

The estimator will also tell you this itself: every report carries back the cache posture and pricing profile it used, and names the term that dominates the bill — dominated by point reads (object GETs). Leave loadsPerMonth unset and it says so in its notes rather than quietly reporting a month with no write side at all.

The one crossover, as a shape

The dashed line does not move — that is what paying for a node instead of for usage looks like. The rising curve is what you pay for the same reads. Where they meet is the rate in the table above, and the shaded halves are which side of it you are on.

Where pay-per-use crosses a flat Redis-HA node aws-us-east-1-ondemand · cache off · flat Redis-HA = $346/mo CloudBitmaps (pay-per-use)Redis HA (flat) Sustained point reads (object GETs, cache off) cheaper than Redis Redis is cheaper $0 $200 $400 $600 0 100 200 300 400 500 reads / sec crossover ≈ 329 /s
Generated by pnpm bench from estimateCost() — the same function the library ships, so the chart cannot disagree with the estimator a caller gets.

What is still owed

The loaded read path has never been timed inside a region, so no latency is published here as the library's. The bill of a cold intersect and of a load is measured; their stopwatch is not, and neither is everything a load or a read can cost. Wall-clock throughput on shared runners is too noisy to gate. The first six rows below are what is still owed, latency and throughput first; the seventh is a measured ceiling, marked as such.

A hundredfold more segments, the same memory

The fear with a library that holds bitmaps is that a long-running server accumulates them until it dies. So the fleet was grown from a thousand segments to a hundred thousand, reading every one, under a fixed reader-cache cap. Retained heap is flat. That flatness is the claim — memory is a function of the cap you set, not of how much data you own.

Memory at fleet scaleMeasured · 1,000 → 100,000 segments
FleetRetained heap (cap 1024)Peak RSSDiscovery scan
1,000 segments8.2 MiB68.1 MiB87.6 ms
10,000 segments8.3 MiB86.8 MiB1,067.9 ms
100,000 segments7.4 MiB162.2 MiB11,606.4 ms

A 100× larger fleet moved retained heap by 0.9 MiB. Intersection of two 2,000,000-id segments (2,000 chunks each, 100 shared) fetched only 100 of the 2,000 chunks per segment, in 24.6 ms. Fleet seeded at ~38–51 durable segments/s (fsync-bound). Measured on Apple M3 Pro (arm64, node v24.18.1) — discovery is filesystem-bound here, so the shape is the claim, not the absolute milliseconds.

Method

Real services

Real S3 in us-east-1 at on-demand rates. No emulator and no simulated latency. The harness rehearses its workload against MinIO for free, and a rehearsal's results file is kept out of git, so what gets published is a real-account run or nothing.

Metered at the wire

Request counts come from the AWS SDK layer, not from the library's metrics sink — which cannot see a PUT at all (it emits no storage.put event, a known observability gap). A PUT bills at 12.5× a GET, so an ingest-heavy workload priced without them is understated.

Gated, both directions

Every money figure on this page, the crossover rate and the calibration run's figures are checked by a build gate against the source that produced them — bench/results.json, docs/benchmarks.md, a calibration run's own results file, the soak's own results, the pricing profile itself — and the check also fails on a figure those sources cannot account for, so a number cannot improve without evidence either. The at-scale table is the exception: nothing checks it yet.

And the estimator itself

The modelled figures here come from estimateCost(), so its accuracy is the claim they rest on. For point reads, priced against the chunk GETs a metrics sink observed, its prediction must land on or above what the engine incurred: it never quotes fewer chunk reads than the engine makes. That is all it covers. For an intersect it prices the chunksPerIntersect you give it, 1 by default, and it has no term yet for a single-bucket store's pointer and tail reads, so for a load or an intersect there it under-quotes, and the measured figures below are the ones to use.

The single-bucket bill · measured, and expected where marked 2026-09-23 · us-east-1 · run 2026-09-23-94416
Operation Per million Requests
Cold A ∩ B, 100 of 1,999 chunks shared, the median $82.40 206 GETs
The same, each pointer read once — expected $81.60 204 GETs
Write and publish a segment $11.20 2 PUT + 3 GET
Write and publish, multipart $26.20 5 PUT-class + 3 GET
A segment's first store.load() — expected $22.80 4 PUT-class + 7 GET

Measured request counts at list prices, and expected where the run did not measure: the same intersect with each pointer read once, as it would be inside the region, and store.load()'s listings and collection pass, which a test counts. All 40 cold intersects — each on a fresh store, with nothing cached — returned the planned ids, checked by count and by sum, and each requested 100 of 1,999 chunks per segment, the chunks the two share, and no other. Each also made one tail read per segment: its last 256 KiB, which holds the footer and the index. PUT-class requests are the ones S3 bills at the PUT rate, listings included. The run's requests, teardown included, cost $0.0034816; data transfer is not in that. The client was a laptop outside the region, so no latency or upload rate from this run is published as the library's: those numbers measured its connection. The run's report explains every figure, in six sections, each with a diagram.

What the metered requests cost · the object-store half 2026-07-25 · us-east-1 · run 2026-07-25-60291
Term Billed quantity Cost
S3 PUT / LIST 22 $0.000110
S3 GET 23 $0.000009

This is half of a run. Its other half metered a NoSQL delta tier the library no longer has, so those line items and the rates behind them are not restated here — republishing them would put a price on a code path you cannot take. Nor is a total: the rows above are two terms out of four, and a “total” over a subset is a number no run produced. The harness that produced the run, and its raw artifact, were removed along with the tier they were built to meter, so these two rows are the record. What is left is the object-store half — and the object store is now the whole write path and the whole read path. Its two unit figures, $0.14 per million count() calls and $5.88 per million publishes, left the pointer out. The count figure is still the one in the cost anchors; the publish figure is superseded by the single-bucket run above, which measured the pointer too.

What these numbers do not establish

Each of these is a real limit on what you should conclude.

Run it against your own rates.

The cost model is a shipped, pure function. Ours is one pricing profile; yours will move every number on this page, including which side of the crossover you are on.

Apache-2.0 · v0.10.0 · pre-1.0 · single maintainer

ReproduceNo cloud account
git clone https://github.com/cloudbitmaps/cloudbitmaps
pnpm install

# regenerates bench/results.json
pnpm bench