# Understanding Local-Echo Deduplication Using the local_event_ids Cache in Buzz

> Learn about local-echo deduplication in Buzz. Discover how the local_event_ids cache prevents duplicate event delivery by filtering Redis echoes before clients receive them.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: deep-dive
- Published: 2026-08-29

---

**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`](https://github.com/block/buzz/blob/main/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:

```rust
/// 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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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:

```rust
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:

```bash
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:

```rust
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`](https://github.com/block/buzz/blob/main/side_effects.rs) and [`event.rs`](https://github.com/block/buzz/blob/main/event.rs).

## Summary

- **Buzz uses `local_event_ids`** to 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.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/handlers/event.rs) by 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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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.