Understanding Local-Echo Deduplication Using the local_event_ids Cache in Buzz
Local-echo deduplication prevents duplicate event delivery by caching recently published event IDs in-memory, filtering out redundant Redis echoes before they reach subscribing clients.
Buzz operates as a multi-tenant Nostr relay where each community maintains isolated Redis fan-out channels. When a client publishes an event, the relay forwards it to Redis for distribution to other instances, but the originating relay also receives that same message back—creating a potential duplicate. To solve this, Buzz implements a specialized caching mechanism using local_event_ids to track in-flight events and suppress local echoes automatically.
The Local-Echo Problem in Buzz's Redis Architecture
In the Buzz relay architecture (block/buzz), each relay instance connects to Redis to broadcast events across the multi-tenant mesh. When a client submits a message through the WebSocket API, the relay publishes the event to a community-specific Redis channel. Because all relay instances subscribe to these channels—including the one that originated the message—the publisher would normally receive its own event back via Redis, resulting in the client seeing the same event twice (once from the immediate publish acknowledgment, once from the Redis subscription).
How the local_event_ids Cache Works
The local_event_ids cache is defined in AppState within crates/buzz-relay/src/state.rs (lines 72-82). It uses the high-performance Moka cache library to store a bounded set of recently published event identifiers:
/// Recently-published event IDs for local-echo deduplication, keyed by
/// `(community_id, event_id)`. Events fanned out in-process are added here;
/// the Redis subscriber consumer skips them to avoid double delivery.
///
/// The community is part of the key because the same Nostr event id can
/// legitimately exist in two communities … Keying on the bare id would let a
/// local publish in community A suppress delivery of a distinct event with the
/// same id arriving via Redis for community B … Entries expire after 60
/// seconds via moka's TTL eviction — bounded regardless of subscriber health.
pub local_event_ids: Arc<moka::sync::Cache<(CommunityId, [u8; 32]), ()>>,
This in-process cache maintains entries for 60 seconds using Moka's TTL eviction, ensuring memory usage remains bounded even if Redis subscribers stall or disconnect.
Step-by-Step Deduplication Flow
The local-echo prevention mechanism follows a three-phase pipeline that automatically filters duplicates without client intervention.
Phase 1: Publishing and Cache Insertion
When processing a client-initiated publish request, the relay inserts the (community_id, event_id) tuple into local_event_ids before forwarding the event to Redis. This insertion occurs in the event-handling pipeline within crates/buzz-relay/src/handlers/side_effects.rs. By marking the event as "locally originated" before Redis fan-out begins, the relay prepares to recognize its own echo.
Phase 2: Redis Fan-Out and Round-Trip
The relay publishes the event to the community's Redis channel. All connected relay instances, including the originator, receive the message through their subscription loops. In crates/buzz-relay/src/main.rs, the subscriber loop invokes sub_registry.fan_out(), which ultimately delegates to the event handler responsible for deduplication checks.
Phase 3: Subscriber Deduplication Check
In crates/buzz-relay/src/handlers/event.rs (lines 301-303), the Redis subscriber handler checks local_event_ids before delivering the event to local WebSocket connections:
if state.local_event_ids.get(&echo_key).is_some() {
state.local_event_ids.invalidate(&echo_key);
// Skip sending the event to the originating client
}
If the cache contains the event key, the handler immediately invalidates the entry and skips the message, preventing the duplicate from reaching the client. If the key is absent (indicating the event arrived from a different relay instance), the event proceeds to normal fan-out.
Why the Composite Key Includes Community ID
Buzz scopes the cache key as (CommunityId, [u8; 32]) rather than using the bare 32-byte Nostr event ID alone. This design prevents cross-tenant deduplication errors, as identical event IDs may legitimately exist in different communities (for example, channel-less broadcasts or distinct channels hashing to the same ID). Keying on the community-event tuple ensures that deduplication in community A never suppresses legitimate message delivery in community B.
Implementation Example
While the deduplication logic runs automatically within the relay, developers can observe the cache behavior through the following patterns.
CLI Usage (Automatic Deduplication)
When using the Buzz CLI to publish messages, the relay handles deduplication transparently:
buzz messages publish --content "Hello, world!" --channel abcdef12-3456-7890-abcd-ef1234567890
The client receives the event immediately upon publishing, but the subsequent Redis echo is suppressed by the local_event_ids check before it can reach the WebSocket connection.
Rust API Reference
For developers extending the relay, the cache interaction follows this pattern:
use buzz_relay::state::AppState;
use std::sync::Arc;
// 1️⃣ Insert when publishing locally
let echo_key = (community_id, event_id);
state.local_event_ids.insert(echo_key, ());
// 2️⃣ Check and invalidate when receiving from Redis
if state.local_event_ids.get(&echo_key).is_some() {
state.local_event_ids.invalidate(&echo_key);
return; // Skip local echo
}
In production, these operations occur automatically within the request handlers defined in side_effects.rs and event.rs.
Summary
- Buzz uses
local_event_idsto prevent duplicate event delivery in multi-tenant Redis architectures. - The cache stores
(CommunityId, EventId)tuples with a 60-second TTL via Moka, preventing unbounded memory growth. - Deduplication occurs in
crates/buzz-relay/src/handlers/event.rsby checking the cache before WebSocket delivery and invalidating the entry upon match. - The composite key design isolates deduplication per community, preventing cross-tenant event suppression as documented in
ARCHITECTURE.md. - The mechanism is fully transparent to clients using the Buzz CLI or SDK.
Frequently Asked Questions
How long do entries remain in the local_event_ids cache?
Entries expire automatically after 60 seconds via Moka's TTL eviction policy. This duration balances the need to catch Redis round-trips (typically milliseconds) while ensuring the cache remains bounded under all subscriber health conditions.
Why does Buzz use a composite key instead of just the event ID?
The cache keys on (CommunityId, [u8; 32]) because Nostr event IDs are not globally unique across communities. The same 32-byte hash could legitimately appear in different tenant channels. Scoping to the community ID prevents deduplication in one tenant from accidentally suppressing valid messages in another.
Where does the cache insertion happen when a client publishes an event?
The insertion occurs in crates/buzz-relay/src/handlers/side_effects.rs within the event-handling pipeline, immediately before the relay publishes to Redis. This ensures the marker exists before the echo returns through the subscription loop.
What happens if the Redis subscriber is temporarily disconnected?
The moka cache's TTL eviction guarantees that entries expire after 60 seconds regardless of subscriber health. If a subscriber reconnects after a delay, any entries older than the TTL will have already been purged, preventing stale deduplication that could block legitimate message delivery.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →