npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cloudbitmaps/roaring

v0.9.0

Published

Distributed, cloud-native Roaring Bitmaps — query and intersect billion-scale integer sets over tiered cloud storage (RAM → NoSQL → object store), at a fraction of an always-on cache.

Readme

@cloudbitmaps/roaring

Distributed, cloud-native Roaring Bitmaps. Query and intersect billion-scale integer sets over tiered cloud storage (RAM → NoSQL → object store) at a fraction of an always-on cache — with the fast, familiar in-memory API: add, has, remove, count, intersect.

This is the flagship flavor of the CloudBitmaps family: the roaring codec (CRoaring, via roaring) plus the CloudRoaring facade, on top of the codec-agnostic @cloudbitmaps/core engine — which arrives transitively, so this is the only package you install.

npm i @cloudbitmaps/roaring @aws-sdk/client-s3 @aws-sdk/client-dynamodb   # + only the SDK(s) you use
import { CloudRoaring } from '@cloudbitmaps/roaring';
import { S3ColdDriver } from '@cloudbitmaps/roaring/s3';
import { DynamoDbWarmDriver } from '@cloudbitmaps/roaring/dynamodb';

Every storage driver is re-exported on a matching subpath (/s3, /gcs, /azure, /dynamodb, /postgres, /redis, /mongodb, /cassandra, /mysql); each backend SDK is an optional peer dependency, so the main entry stays lean. Ships two CLIs: compact-segments and export-segments.

Measured, not modeled

Benchmarked against real S3 + DynamoDB in us-east-1, not an emulator: $0.75 per million incremental writes, $0.14 per million count()s, $5.88 per million segment publishes — against an always-on Redis-HA line of $346/month, standing. DynamoDB capacity is taken from AWS's own ConsumedCapacity rather than estimated. count() on a published segment does 0 payload reads, and intersecting two 2,000,000-id segments fetches only the shared chunks.

The trade is stated plainly rather than buried: a membership check that misses the hot cache costs a network round trip to your warm tier, where an in-process RAM store costs a memory read. If you need a sub-millisecond p99 on a working set that fits a bounded hot cache, use Redis. If your sets are large, mostly read, and shouldn't cost $346/month to keep warm, use this.

Coming from Redis bitmaps?

A durable alternative to Redis bitmaps for set-shaped work — audiences, dedup, suppression, membership — where the sets are large, mostly idle, must survive a restart, and are written in batches. You are not giving up the bitmap either: past 4,096 ids in a 65,536-id chunk — 6.25% of it — Roaring stores that chunk as a flat bit array, byte for byte what you have now. It just stops paying for the chunks you never wrote to.

| Redis | Here | |---|---| | SETBIT / GETBIT | add / addMany · remove / removeMany · has | | prior = SETBIT | claimMany(ids) — adds a batch and returns only the ids that were not already there, so an "already sent to this id?" check is exactly-once | | BITCOUNT | count() — exact, served from the index with no payload reads | | BITOP AND / OR / DIFF | intersect / union / andNot — and intersect skips chunks that cannot contribute | | EXPIRE | setRetention(ref, { expiresAt }) + retireExpired() — a per-segment expiry the writer sets, and a sweep you schedule (this library starts no timer, so it works the same in a Lambda and a server). No per-id TTL: a bitmap stores ids, not timestamps. And never enable your backend's own row expiry on the warm table — those rows are un-compacted deltas, so it discards writes silently |

It is not a drop-in replacement, and two limits are worth knowing before you port anything.

1. Do not port a per-id write loop. SETBIT flips one bit in place and is genuinely O(1). Here a warm write re-serializes a whole 65,536-id chunk, so 5,000 ids added one at a time cost 5,000 writes and 23,762 KB, against 1 write and 8 KB for the same ids in one call — roughly 3,000× the bytes. Batch and you are far cheaper than Redis; port the loop literally and you are far more expensive. On a per-request-metered store (DynamoDB on-demand) that is a line item; on an instance-priced one (ElastiCache, RDS) it surfaces as IOPS and latency headroom instead, which makes it easier to miss rather than cheaper. claimMany takes a batch for exactly this reason.

2. What does not carry over: the raw bytes. A .crbm object is not a flat bit array, so anything reading your Redis bitmap's underlying string — a job that GETs the key and indexes into it, a byte-for-byte backup — will not read ours. BITFIELD, BITPOS, BITOP NOT and byte-range BITCOUNT have no equivalent either: this is a set of ids, not an addressable bit buffer, and NOT in particular has no bounded universe to complement against. Raw bit-position import/export is unbuilt; say so in an issue if you need it, because that is what decides whether it gets built.

Retiring data: a per-segment expiry, and a sweep you schedule

A segment can expire; an individual id cannot (a bitmap stores ids, not (id, timestamp) pairs — a timestamp per id costs more than the compression saves).

const DAY = 86_400_000;
const ref = { namespace: 'active-daily', segment: '2026-08-05' };

await store.setRetention(ref, { expiresAt: Date.now() + 30 * DAY }); // once, when you create the bucket

expiresAt is an absolute instant you compute, not a duration derived from anything observed: compaction rewrites every basis such a duration could use, so "30 days since the last write" would keep a busy bucket alive precisely because the daemon was keeping it cheap.

Then, from whatever schedule your deployment already has — an EventBridge rule, a Kubernetes CronJob, cron, a queue job — run the sweep. This library starts no background timer, deliberately: the same code has to behave identically in a Lambda, an edge isolate and a long-lived server, and a timer that only works in one of those is worse than none.

const swept = await store.retireExpired({ namespace: 'active-daily' });
for (const e of swept.entries) if (e.action === 'skipped') console.warn(e.segment, e.reason);
if (swept.limited) scheduleAnotherPassSoon(); // the per-cycle cap deferred some; re-run

Each retirement goes through dropSegment, so the Warm → registry → Cold ordering is one implementation rather than two. The sweep is bounded (limit, default 100), previewable (dryRun), and returns a per-segment ledger rather than throwing — a fault on one segment must not decide the fate of the other ninety-nine. Once a day is enough for daily buckets. Full walkthrough: getting-started §13.5.

No seed step, and compaction is optional

A brand-new segment is usable the moment you construct the store: addMany, has, remove, count, iterate and the whole set algebra work against a segment with no cold generation and no registry row, because a read merges (cold ∪ warm.adds) \ warm.removes and an absent cold tier just makes that merge trivial. Bulk-load is an import path for data you already have elsewhere, not an initialization step.

Compaction is a cost optimization, never a correctness requirement — it buys cheaper storage, smaller rewrites, and a free index-only count(). For a write-once dated bucket you retire (with dropSegment, or by recording an expiry as above), skip it entirely.

Redis stays first-class as a warm tier underneath this (@cloudbitmaps/roaring/redis) — the point above is about replacing SETBIT-on-one-giant-key as your data model, not replacing Redis as infrastructure.

Full README, guides, benchmarks (with the method and what the numbers do not establish), and the design corpus live in the repository. Licensed Apache-2.0.