# Moka Eviction Listeners Example: Observing Cache Removals in Rust

> Explore Moka eviction listeners in Rust. See how to observe cache removals for expirations, replacements, and size-based evictions with an easy-to-follow example.

- Repository: [moka-rs/moka](https://github.com/moka-rs/moka)
- Tags: example
- Published: 2026-03-07

---

**Moka's eviction listeners are user-supplied callbacks that receive the key, value, and `RemovalCause` whenever an entry is removed from the cache, enabling you to react to expirations, replacements, and size-based evictions.**

The `moka-rs/moka` crate provides high-performance concurrent caching for Rust with built-in support for eviction notifications. Understanding how to implement a **Moka eviction listeners example** allows you to track cache lifecycle events, perform cleanup operations, or audit removals in both synchronous and asynchronous contexts.

## How Eviction Listeners Work in Moka

Moka implements eviction listeners through a type-erased callback system that supports both synchronous and asynchronous execution. The implementation spans multiple core files to separate registration, storage, and safe execution.

Key components include:

- **`CacheBuilder::eviction_listener`** in [`src/sync/builder.rs`](https://github.com/moka-rs/moka/blob/main/src/sync/builder.rs) (lines 25-46) – Registers a synchronous listener as an `Arc<dyn Fn(Arc<K>, V, RemovalCause) + Send + Sync>`
- **`CacheBuilder::async_eviction_listener`** in [`src/future/builder.rs`](https://github.com/moka-rs/moka/blob/main/src/future/builder.rs) (lines 350-360) – Registers an async listener returning a `ListenerFuture`
- **`RemovalCause` enum** in [`src/notification.rs`](https://github.com/moka-rs/moka/blob/main/src/notification.rs) (lines 29-46) – Defines why entries disappear: `Expired`, `Explicit`, `Replaced`, or `Size`
- **`RemovalNotifier`** in [`src/notification/notifier.rs`](https://github.com/moka-rs/moka/blob/main/src/notification/notifier.rs) (lines 25-42) – Executes listeners inside `catch_unwind` panic guards and disables them after panics

### The RemovalCause Enum

The `RemovalCause` type provides granular context about why an entry was removed. In [`src/notification.rs`](https://github.com/moka-rs/moka/blob/main/src/notification.rs), the enum defines four variants:

- **`Expired`** – The entry's time-to-live (TTL) or time-to-idle (TTI) expired
- **`Explicit`** – The entry was manually removed via `invalidate()` or `invalidate_all()`
- **`Replaced`** – A new value was inserted for an existing key
- **`Size`** – The entry was evicted to maintain the cache's maximum capacity

Each variant includes the `was_evicted()` helper method to distinguish between true evictions (`Replaced`, `Size`) and explicit removals.

## Registering a Synchronous Eviction Listener

For synchronous caches, register a listener during the build phase. The closure receives owned copies of the key and value along with the removal cause.

The following example, adapted from [`examples/eviction_listener_sync.rs`](https://github.com/moka-rs/moka/blob/main/examples/eviction_listener_sync.rs), demonstrates tracking size-based evictions, replacements, and TTL expirations:

```rust
use moka::sync::Cache;
use std::{thread::sleep, time::Duration};

fn main() {
    // Small cache, 1 second TTL, to see evictions quickly.
    let ttl = 1;
    let cache = Cache::builder()
        .max_capacity(2)
        .time_to_live(Duration::from_secs(ttl))
        .eviction_listener(|key, value, cause| {
            // `key` and `value` are cloned/owned, `cause` tells why it was removed.
            println!("Evicted ({key:?}, {value:?}) because {cause:?}");
        })
        .build();

    // Fill the cache – the third insertion forces a `Replaced` eviction.
    cache.insert(&0, "zero".to_string());
    cache.insert(&1, "one".to_string());
    cache.insert(&2, "twice".to_string()); // evicts entry 0 (Size)
    cache.insert(&2, "two".to_string());   // evicts entry 2 (Replaced)

    // Wait for TTL-based eviction.
    sleep(Duration::from_secs(ttl + 1));

    // Explicit removal.
    if let Some(v) = cache.remove(&2) {
        println!("Removed explicitly: {v}");
    }

    // Force all pending background removals and invoke listeners.
    cache.invalidate_all();
    while cache.entry_count() > 0 {
        cache.run_pending_tasks();
    }
}

```

In this example, the listener fires for three distinct reasons: capacity constraints trigger `Size`, updating an existing key triggers `Replaced`, and the TTL expiration triggers `Expired`. The final loop ensures background eviction tasks complete by calling `run_pending_tasks()` until `entry_count()` reaches zero.

## Using Async Eviction Listeners

The asynchronous cache in [`src/future/cache.rs`](https://github.com/moka-rs/moka/blob/main/src/future/cache.rs) supports non-blocking eviction handlers through `async_eviction_listener`. Unlike the synchronous version, the async listener must return a boxed future (`ListenerFuture`) that the cache's housekeeping task awaits sequentially.

This pattern prevents background eviction from outrunning your async cleanup code:

```rust
use moka::future::Cache;
use std::{sync::Arc, time::Duration};

#[tokio::main]
async fn main() {
    let cache = Cache::builder()
        .max_capacity(2)
        .time_to_live(Duration::from_secs(1))
        .async_eviction_listener(|key, value, cause| {
            // Return a future that does the work.
            Box::pin(async move {
                println!("Async evicted ({:?}, {:?}) because {:?}", key, value, cause);
                // Simulate I/O or other async work here.
            })
        })
        .build();

    cache.insert(0, "zero".to_string()).await;
    cache.insert(1, "one".to_string()).await;
    cache.insert(2, "two".to_string()).await; // triggers eviction
    tokio::time::sleep(Duration::from_secs(2)).await; // TTL eviction

    // `invalidate_all` triggers async notifications for remaining entries.
    cache.invalidate_all().await;
}

```

The async implementation stores the listener as `Option<AsyncEvictionListener<…>>` and clones it for each cache segment in [`src/sync/segment.rs`](https://github.com/moka-rs/moka/blob/main/src/sync/segment.rs), ensuring thread-safe access across concurrent operations.

## Panic Safety and Listener Lifecycle

Moka guards against panicking listeners through `RemovalNotifier::notify` in [`src/notification/notifier.rs`](https://github.com/moka-rs/moka/blob/main/src/notification/notifier.rs). The notifier wraps each listener call in `std::panic::catch_unwind`, converting panics into logged warnings rather than cache crashes.

If a listener panics, the notifier sets its internal `is_enabled` flag to `false`, permanently disabling further notifications while keeping the cache operational:

```rust
use moka::sync::Cache;

fn main() {
    let cache = Cache::builder()
        .max_capacity(1)
        .eviction_listener(|_k, _v, _c| panic!("oops!"))
        .build();

    cache.insert(&1, "one".to_string()); // first entry
    cache.insert(&2, "two".to_string()); // causes eviction → listener panics

    // The listener is now disabled; further evictions will NOT call it.
    cache.insert(&3, "three".to_string()); // silent eviction
}

```

This safety mechanism ensures that a buggy eviction listener cannot destabilize production systems. When the `logging` feature is enabled, Moka logs the panic reason before disabling the notifier.

## When Eviction Listeners Are Not Invoked

Not all cache removals trigger eviction listeners. Understanding these boundaries is crucial for reliable cache instrumentation:

- **Cache dropping** – When the `Cache` object is dropped, entries are destroyed without invoking listeners. Use `cache.invalidate_all()` (or `invalidate_all().await` for async) before dropping if you require notifications for every entry.
- **Delayed background tasks** – In synchronous caches, background invalidations may delay if `run_pending_tasks()` is not called periodically. Loop until `entry_count() == 0` to guarantee delivery of all pending notifications.

## Summary

- **Moka eviction listeners** provide callbacks that receive `Arc<K>`, owned `V`, and `RemovalCause` whenever entries are removed from the cache.
- Register synchronous listeners via `CacheBuilder::eviction_listener` in [`src/sync/builder.rs`](https://github.com/moka-rs/moka/blob/main/src/sync/builder.rs) or async variants via `async_eviction_listener` in [`src/future/builder.rs`](https://github.com/moka-rs/moka/blob/main/src/future/builder.rs).
- The `RemovalCause` enum distinguishes between `Expired`, `Explicit`, `Replaced`, and `Size` removals.
- **Panic safety** is enforced by `RemovalNotifier` in [`src/notification/notifier.rs`](https://github.com/moka-rs/moka/blob/main/src/notification/notifier.rs), which catches panics and disables the listener to prevent cache crashes.
- **Async listeners** return boxed futures that the cache awaits sequentially, ensuring background eviction synchronization.
- Listeners do not fire when the cache is dropped; explicitly call `invalidate_all()` to trigger notifications for remaining entries.

## Frequently Asked Questions

### What is an eviction listener in Moka?

An eviction listener is a user-provided callback function that Moka executes whenever an entry is removed from the cache. According to the `moka-rs/moka` source code, the listener receives the key as an `Arc<K>`, the value as an owned `V`, and a `RemovalCause` enum indicating whether the removal was due to expiration, explicit invalidation, replacement, or size constraints.

### How do I ensure my eviction listener fires for all entries?

To guarantee your eviction listener processes every removal, explicitly call `invalidate_all()` before the cache is dropped. Simply dropping the cache destroys entries without triggering listeners. For synchronous caches, you may also need to loop `run_pending_tasks()` until `entry_count()` reaches zero to flush pending background evictions.

### Can eviction listeners panic without crashing the cache?

Yes. The `RemovalNotifier` implementation in [`src/notification/notifier.rs`](https://github.com/moka-rs/moka/blob/main/src/notification/notifier.rs) wraps listener calls in `catch_unwind`. If your listener panics, Moka catches the panic, disables further listener invocations by setting `is_enabled` to `false`, and optionally logs the error. This ensures the cache remains operational even if the listener contains bugs.

### What is the difference between sync and async eviction listeners?

Synchronous listeners implement `Fn(Arc<K>, V, RemovalCause)` and execute immediately on the current thread during removal operations. Async listeners, registered via `async_eviction_listener`, return a boxed `ListenerFuture` that the cache's internal housekeeping task awaits. This design allows async listeners to perform I/O or other asynchronous work without blocking cache operations, as implemented in [`src/future/cache.rs`](https://github.com/moka-rs/moka/blob/main/src/future/cache.rs).