Overview
LaserStream supports compressed account filtering via cuckoo filters. Instead of sending an explicit pubkey list in your subscribe request (32 bytes per account), you send a compact probabilistic filter that costs roughly 3–4 bytes per account on the wire. This makes it practical to subscribe to hundreds of thousands of accounts in a single stream — no sharding across connections, no oversized subscribe requests. For example, a filter tracking 500,000 accounts serializes to about 2.1 MB, versus 16 MB as a raw pubkey list — roughly 7.6x smaller. The exact savings depend on how full the filter is: the closer it is to capacity, the fewer bytes per account.Availability
| Client | Minimum version | Cuckoo support |
|---|---|---|
LaserStream SDK — Rust (helius-laserstream) | 0.2.0 | ✅ |
LaserStream SDK — JavaScript/TypeScript (helius-laserstream) | 0.4.0 | ✅ |
| LaserStream SDK — Go | — | ❌ Not yet |
Yellowstone gRPC — Rust (yellowstone-grpc-client) | 13.1.0 | ✅ |
When to use cuckoo filters
| Tracked accounts | Recommended approach |
|---|---|
| Up to ~10,000 | Explicit pubkey lists (account: [...]) — simple and exact |
| ~10,000 and above | Cuckoo filter via CompressedAccountFilterSet |
How it works
- Build the filter client-side. Insert each tracked pubkey into a
CompressedAccountFilterSet. The hash seed is randomized per filter and serialized alongside it, so the server hashes incoming accounts with the same seed your client used. - Attach it to your subscribe request.
insert_into_subscribe_request()places the serialized filter into the accounts stream of a standardSubscribeRequest. - The server matches probabilistically. Because the filter is probabilistic, the server may deliver updates for accounts you didn’t track — false positives are bounded at under 1% at full load. There are never false negatives: every update for a tracked account is delivered.
- Re-check each update locally — this step is required. Call
set.contains(pubkey)on every incoming account before processing it. This check is exact (backed by an internal hash set), so after local filtering you see zero false positives.
Quickstart (Rust)
Add the SDK to your project:Cargo.toml
main.rs
rust/examples/cuckoo_account_filter.rs.
Quickstart (JavaScript/TypeScript)
Install the SDK (cuckoo support requireshelius-laserstream 0.4.0+):
javascript/examples/cuckoo-account-sub.ts.
API reference
CompressedAccountFilterSet wraps the raw cuckoo filter together with an exact hash set, so mutations and membership checks are always safe and exact:
| Method | Behavior |
|---|---|
with_capacity(n) | Create a filter sized for n tracked accounts. Size for your peak tracked-set size. |
insert(pubkey) | Returns Ok(true) if new, Ok(false) if a duplicate, Err(TableFullError) if the filter is at capacity. |
remove(pubkey) | Removes the account. Safe and exact. |
contains(pubkey) | Exact membership check — use this to scrub server-side false positives. |
insert_into_subscribe_request(&mut request, "label") | Attach the filter to the accounts stream of a SubscribeRequest. |
to_account_filter() / to_proto() | Lower-level conversions for custom request assembly. |
is_dirty() / take_dirty() | Report whether the set changed since it was last placed into a request — useful for resubscribe cycles. |
new CompressedAccountFilterSet(capacity) instead of with_capacity, insertIntoSubscribeRequest, isDirty, takeDirty, toProto, and so on. In JavaScript insert returns a boolean (true if newly added) and throws TableFullError when the filter is saturated. A pubkey can be passed as a base58 string, raw 32 bytes, or any object with a toBytes() method.
Always use CompressedAccountFilterSet rather than the raw CuckooFilter it wraps. The raw filter’s remove() can silently remove the wrong item — a documented footgun of cuckoo filters. The wrapper pairs the filter with an exact hash set, so insert, remove, and contains are always correct.
Capacity sizing
- Size the filter for the peak number of accounts you expect to track via
with_capacity(n). - Inserting beyond capacity fails gracefully with a
TableFullError— the filter is never corrupted. In practice the table tolerates slight overfill before rejecting inserts, but don’t rely on that headroom. - The serialized size is determined by capacity, not by how many accounts you’ve inserted — so an oversized filter wastes wire bytes. Pick a capacity close to your real peak.
Updating the tracked set
When your tracked set changes (new accounts to follow, old ones to drop):- Call
insert()/remove()on theCompressedAccountFilterSet. - Check
is_dirty()(or consume the flag withtake_dirty()) to see whether the filter changed since it was last sent. - If dirty, rebuild the request with
insert_into_subscribe_request(). In JavaScript you can re-send it on the same stream withstream.write(request); in Rust, resubscribe with the rebuilt request.
FAQ
Can I miss updates for accounts in my filter?
Can I miss updates for accounts in my filter?
No. Cuckoo filters produce false positives (extra updates for untracked accounts) but never false negatives. Every update for a tracked account is delivered.
How many extra (false-positive) updates will I receive?
How many extra (false-positive) updates will I receive?
Under 1% at full load, and typically less when the filter is below capacity. One local
contains() call per update filters them out exactly.Which clients support cuckoo filters?
Which clients support cuckoo filters?
The Rust SDK (
helius-laserstream 0.2.0+), the JavaScript/TypeScript SDK (helius-laserstream 0.4.0+), and the Yellowstone Rust client (yellowstone-grpc-client 13.1.0+). The Go SDK does not support it yet. See the availability table above.Can I still use explicit pubkey lists?
Can I still use explicit pubkey lists?
Yes. Standard
account: [...] filters work unchanged and remain the right choice for small account sets (up to roughly 10,000 accounts). See the account subscription guide.Related
Account Subscriptions
Standard account filtering with owner, datasize, and memcmp filters.
Clients & SDKs
TypeScript, Rust, and Go SDKs with automatic replay and reconnects.