Skip to main content

Compressed Filters (Cuckoo Filters)

Updated on
Jul 02, 2026
Solana gRPC is included with Scale and Business plans. On Build and Accelerate plans, it remains available via the Solana gRPC add-on.

Overview

Compressed filters (also known as cuckoo filters) let you track millions of accounts in a single Solana gRPC subscription without hitting pubkey limits. Instead of sending an explicit list of pubkeys, you send a compact binary filter that the server uses to match incoming updates.

A standard Solana gRPC subscription carries an explicit list of the pubkeys you want to match, and that list grows large fast: one million pubkeys is roughly 44 MB per request. Tracking hundreds or thousands of accounts (e.g., 500 wallets plus 700 token accounts) quickly exceeds those limits.

A compressed filter replaces that explicit list with a probabilistic data structure that encodes the same set in a fraction of the space:


  • Bypasses pubkey limits: bounded only by message size (~99 MiB, which supports tens of millions of pubkeys).
  • Reduces payload size by ~10x compared to an explicit pubkey list.
  • Supports O(1) insert and remove operations, so you can update a large set in place instead of rebuilding it from scratch.

What Is a Compressed Filter?

A compressed filter is a probabilistic data structure that tests whether a pubkey belongs to a set without storing the full set. Instead of keeping each 32-byte pubkey, it stores a short fingerprint (a small hash) of each one in a table of buckets, cutting the space needed to represent the set by roughly 10x.

Account CountCompressed FilterExplicit Pubkey List
1,000~4 KiB~44 KB
10,000~32 KiB~440 KB
100,000~256 KiB~4.4 MB
1,000,000~4 MiB~44 MB
2,000,000~8 MiB~84 MB

How It Works

Building and using a compressed filter follows the same lifecycle regardless of language:


  1. Client builds the filter: Insert every pubkey you want to track into a CompressedAccountFilterSet. The set keeps an exact copy of your pubkeys locally and a compact cuckoo filter for the wire.
  2. Filter is serialized: The cuckoo filter is encoded as a binary blob plus metadata (bucket count, entries per bucket, fingerprint bits, and hash seed) and attached to your subscription request.
  3. Server matches probabilistically: For each incoming update, the server hashes the pubkey and checks whether its fingerprint exists in your filter, forwarding the update if it matches.
  4. Client verifies matches: Because the server-side check has a ~1% false positive rate, your client re-checks each match against its exact pubkey set before acting on it.

Filter Structure

The compressed filter is sent as part of your subscription request:

{
"cuckooAccountInclude": {
"data": "AAAAAAAAAACJdcGaAAAAAAAAAAAASsE3BAAAADxkwAAAAAAAAAAAA...",
"bucketCount": 16,
"entriesPerBucket": 4,
"fingerprintBits": 16,
"hashSeed": "8749487436367949345",
"hashAlgorithm": "SIP_HASH"
}
}
FieldDescription
dataBase64-encoded bucket data
bucketCountNumber of buckets in the filter
entriesPerBucketSlots per bucket (typically 4)
fingerprintBitsBits per fingerprint (8, 12, or 16)
hashSeedSeed for the hash function (SipHash-2-4)
hashAlgorithmHashing algorithm identifier (currently SIP_HASH)

Subscription Types

Compressed filters can be attached to two subscription types, each firing on a different event:


  • Accounts Filter (cuckoo_accounts_filter): Fires when a tracked account's own state is written to. Use this when you want to monitor account data changes.
  • Blocks Filter (cuckoo_account_include): Fires when any tracked pubkey is referenced in a transaction's account keys, including program IDs used via CPI. Use this for transaction monitoring.

CompressedAccountFilterSet (from the cuckoo module in the yellowstone-grpc-proto crate) provides a method for each:

// Accounts subscription - Attaches the filter to an accounts subscription
filter.insert_into_subscribe_request(&mut request, "tracked");

// Blocks subscription - Attaches the filter to a blocks subscription
filter.insert_into_block_subscribe_request(&mut request, "tracked_blocks");

Code Example

The example below builds a cuckoo filter and attaches it to an Accounts subscription to track account-state changes for specific accounts. The server matches incoming account updates against the filter, then the client re-verifies each match against its exact pubkey set.

Cargo.toml
[package]
name = "compressed-filter-test"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1"
bs58 = "0.5"
futures = "0.3"
rustls = { version = "0.23", default-features = false, features = ["ring"] }
solana-pubkey = "4"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tonic = { version = "0.14", features = ["tls-native-roots"] }
yellowstone-grpc-client = "13.1.0"
yellowstone-grpc-proto = "12.4.0"

src/main.rs
use {
futures::stream::StreamExt,
solana_pubkey::Pubkey,
std::str::FromStr,
tonic::transport::channel::ClientTlsConfig,
yellowstone_grpc_client::GeyserGrpcClient,
yellowstone_grpc_proto::{
cuckoo::CompressedAccountFilterSet,
prelude::{subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest},
},
};

// Quicknode gRPC endpoints split into a URL (with the :10000 port) and a token.
// Example: https://docs-demo.solana-mainnet.quiknode.pro:10000 + abcde123456789
const ENDPOINT: &str = "https://your-solana-grpc-endpoint.quiknode.pro:10000";
const TOKEN: &str = "SOLANA_GRPC_TOKEN";

// Accounts to track. A cuckoo filter matches on account-data updates, so list
// accounts that actually change (mints, token accounts, oracles). The payoff
// grows with the set: it stays ~3 bytes per key, so tracking millions is cheap.
const TRACKED_ACCOUNTS: &[&str] = &[
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT mint
];

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// tonic pulls in rustls 0.23, which no longer auto-selects a crypto backend;
// install one before the first TLS handshake or connect() panics.
rustls::crypto::ring::default_provider()
.install_default()
.map_err(|_| anyhow::anyhow!("failed to install rustls crypto provider"))?;

let pubkeys: Vec<Pubkey> = TRACKED_ACCOUNTS
.iter()
.map(|s| Pubkey::from_str(s).map_err(|e| anyhow::anyhow!("invalid pubkey {s}: {e}")))
.collect::<anyhow::Result<_>>()?;

// Leave headroom above the entry count so inserts don't saturate the table.
let mut filter = CompressedAccountFilterSet::with_capacity(pubkeys.len().max(100))?;
for pk in &pubkeys {
filter.insert(*pk)?;
}
println!("Tracking {} accounts via cuckoo filter", filter.len());

let mut client = GeyserGrpcClient::build_from_shared(ENDPOINT.to_string())?
.x_token(Some(TOKEN))?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.connect()
.await?;

// Attach the cuckoo filter to an accounts subscription; the server matches
// incoming account updates against it.
let mut request = SubscribeRequest {
commitment: Some(CommitmentLevel::Processed as i32),
..Default::default()
};
filter.insert_into_subscribe_request(&mut request, "cuckoo_accounts");

let mut stream = client.subscribe_once(request).await?;
println!("Subscribed with cuckoo filter; waiting for matching account updates...");

while let Some(update) = stream.next().await {
match update?.update_oneof {
Some(UpdateOneof::Account(acc)) => {
let Some(info) = acc.account else { continue };

// The server-side cuckoo check has a ~1% false positive rate.
// contains() is an exact membership test, so use it to confirm.
let matched = <[u8; 32]>::try_from(info.pubkey.as_slice())
.map(|bytes| filter.contains(Pubkey::new_from_array(bytes)))
.unwrap_or(false);

if matched {
println!(
"Account update: {} (slot {})",
bs58::encode(&info.pubkey).into_string(),
acc.slot,
);
}
}
Some(UpdateOneof::Ping(_)) => println!("Ping received - connection alive"),
_ => {}
}
}

Ok(())
}

Best Practices

1. Client-Side Verification

Always re-check matches against your exact pubkey set. The server-side cuckoo match has a ~1% false positive rate, while contains checks the local exact set:

if filter.contains(incoming_pubkey) {
// This is a real match from your tracked set.
}

2. Filter Capacity

Size the filter for the number of pubkeys you track, plus a little headroom for future inserts. Capacity is the maximum number of entries the filter can hold:

let mut filter = CompressedAccountFilterSet::with_capacity(pubkeys.len().max(100))?;

3. Dynamic Updates

Cuckoo filters support O(1) insert and remove operations, so you can adjust a large set without rebuilding it:

filter.insert(new_pubkey)?;
filter.remove(old_pubkey);
filter.insert_into_block_subscribe_request(&mut request, "tracked_blocks");

4. Choose the Right Subscription Type


  • Use the Accounts subscription (cuckoo_accounts_filter) to track account state changes.
  • Use the Blocks subscription (cuckoo_account_include) to track transaction references.

Requirements


Resources


We Love Feedback!

If you have any feedback or questions about this documentation, let us know. We'd love to hear from you!

Share this doc