Bandwidth‑Constrained Waku v2: Using Filter and Store to Lighten Live Subscriptions
A lightweight client can avoid full gossipsub mesh by using Waku v2's Filter and Store protocols for live and backfilled messages.
06 Mar 2026, 20:20 UTC

Problem and Takeaway
You’re on a low‑bandwidth device and need live chat messages without running a full relay node. The practical takeaway is that Waku v2 lets you enable only the Filter protocol for live matching traffic and the Store protocol for historical backfill, avoiding a full gossipsub mesh while still getting usable data.
Why a full gossipsub relay isn’t needed
Waku v2 is a modular protocol suite. A node can enable just the filter and store modules and skip the relay (gossipsub) module entirely. The filter module lets a light client register a content filter with a full node; only messages that match the filter are pushed, cutting bandwidth dramatically compared with joining the relay mesh and discarding non‑matching traffic locally.
Filter protocol basics
The filter protocol works by having a light client send a filter request to a trusted full node. The request contains a content topic (or a more complex filter expression). The full node keeps the subscription and streams only matching messages to the client. This means the client never sees the flood of unrelated gossip, which is ideal when the network link is constrained.
Store protocol for backfill
After a reconnection or when the live filter stream stops, the client can request past messages from a store node. The store protocol supports a time range query and returns a cursor for paging. It complements the live filter stream, but each store node provides its own view of history; ordering and completeness are per‑node, not global.
Worked example with js-waku
Below is a minimal, runnable snippet using js-waku 2.0.0. Adjust the placeholders as noted.
// 1️⃣ Install the exact version
// npm install js-waku@2.0.0
const { Waku } = require('js-waku');
// 2️⃣ Create a client for a private content topic
const client = new Waku({
// placeholder for the content topic you want to use
topic: '',
// use a local in‑process key pair for simplicity
privateKey: '',
// optional: set a short-lived identity if you prefer
});
// 3️⃣ Enable the filter protocol (live stream)
client.enableFilter({
// the filter expression – here we accept all messages on the topic
// you can replace this with a more specific expression
filter: '',
// the address of a full node that will serve the filter
// e.g. 'wss://fullnode.example.com:4001'
server: '',
});
// 4️⃣ Enable the store protocol for backfill
client.enableStore({
// the store node address (can be the same as the filter server)
server: '',
// optional retention period in seconds; default is 24 h
retention: 86400,
});
// 5️⃣ Publish a test message (run on any node that has publishing rights)
client.publish('');
// 6️⃣ Subscribe to live messages (this runs in the same process)
client.on('message', (msg) => {
console.log('Live message:', msg);
});
// 7️⃣ After a short pause, request backfilled history
setTimeout(async () => {
const { messages, cursor } = await client.getStoreMessages({
topic: '',
// ask for the last 10 minutes
start: Date.now() - 10 * 60 * 1000,
end: Date.now(),
// page size – adjust as needed
limit: 100,
});
console.log('Backfilled messages:', messages.length);
// Verify that the live message appears in the backfilled set
const liveIndex = messages.findIndex(m => m.id === msg.id);
if (liveIndex !== -1) {
console.log('✅ Live message found in backfill');
} else {
console.log('⚠️ Live message missing from backfill – check retention or node sync');
}
// Optional: query a second independent store node and compare counts
// const client2 = new Waku({ /* same config */ });
// const { messages: msgs2 } = await client2.getStoreMessages({ /* same params */ });
// console.log('Store node 1 count:', messages.length, 'Store node 2 count:', msgs2.length);
}, 5000);
Where to run: This code runs in any Node.js environment (e.g., a local machine or a cloud VM). No special OS permissions are required beyond Node’s network access.
Placeholders explained: Replace <CONTENT_TOPIC> with the topic you wish to use (e.g., demo/chat). Replace <YOUR_PRIVATE_KEY> with a generated key pair; the example uses an in‑process key for brevity. <FILTER_EXPRESSION> can be a simple topic match or a more complex filter; <FULL_NODE_WSS> and <STORE_SERVER_WSS> are the WebSocket URLs of a full node that supports the filter and a store node respectively.
Trade‑off and limitation
The main trade‑off is trust. The filter node can withhold matching messages, and the store node can omit or reorder history. A robust client therefore queries multiple store nodes, compares cursors, and treats the retrieved history as best‑effort rather than authoritative. Additionally, enabling store means you must configure retention and decide which topics to index; disk growth and query latency depend on traffic volume and the chosen retention window, so you must balance storage cost against the need for recent history.
Actionable closing checklist
- Run two independent Waku v2 nodes with
storeandfilterenabled on a private content topic. - Publish a message from one node and verify the light client receives it via the filter stream.
- After reconnecting, issue a
getStoreMessagesquery to the first store node and note the returned cursor and message count. - Repeat the store query against a second store node and compare the counts and cursors; discrepancies indicate omission or retention gaps.
- Adjust retention settings or add more store nodes if the backfilled set does not include the expected messages.
- Periodically re‑run the snippet after a minor library upgrade (e.g., bump js-waku to the next patch version) to ensure API compatibility.
By following these steps you can build a bandwidth‑friendly Waku v2 client that receives live updates and reliable backfill without the overhead of a full relay node, while staying aware of the trust and configuration trade‑offs inherent in the design.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.