Moka Weigher Closure Usage: Configuring Size-Aware Caching in Rust

Moka's weigher closure allows you to assign custom u32 weights to cache entries by providing a function that calculates size based on key-value pairs, enabling memory-bounded caching instead of entry-count limits.

The moka-rs/moka crate provides high-performance concurrent caching for Rust applications. When you need to limit cache memory consumption rather than just the number of entries, understanding Moka weigher closure usage becomes essential for implementing size-aware eviction policies.

What Is the Moka Weigher Closure?

By default, Moka treats every cache entry as having a weight of 1, meaning max_capacity limits the total number of entries. The weigher closure changes this behavior by allowing you to specify a custom weight calculation function. This function receives references to the key and value and returns a u32 representing the entry's relative cost or size.

How the Weigher Works Internally

Understanding the internal flow helps clarify how your closure integrates with the cache lifecycle.

Type Definition in concurrent.rs

The weigher is defined as a type alias in src/common/concurrent.rs at lines 22-23:

type Weigher<K, V> = Arc<dyn Fn(&K, &V) -> u32 + Send + Sync + 'static>;

This definition requires your closure to be thread-safe (Send + Sync) and have a 'static lifetime, as the cache may invoke it from multiple threads during its entire lifetime.

Builder API in builder.rs

The public API surface resides in src/sync/builder.rs at lines 414-422. The CacheBuilder::weigher method accepts your closure, wraps it in an Arc, and stores it in the builder struct:

pub fn weigher(
    self,
    weigher: impl Fn(&K, &V) -> u32 + Send + Sync + 'static,
) -> Self {
    // Stores the closure for use during cache construction
}

Runtime Calculation in base_cache.rs

During insertion, src/sync/base_cache.rs at lines 40-42 invokes the stored closure:

fn weigh(&self, key: &K, value: &V) -> u32 {
    // If weigher exists, call it; otherwise return 1
}

The returned weight is stored in the entry's EntryInfo as policy_weight and used by the eviction policy (TinyLFU or LRU) to maintain the total weighted_size below max_capacity.

Configuring a Weigher in Your Application

Basic Synchronous Usage

The most common pattern uses byte length as weight. This example from examples/size_aware_eviction_sync.rs demonstrates limiting the cache to approximately 32 MiB:

use moka::sync::Cache;

fn main() {
    let cache = Cache::builder()
        .weigher(|_key, value: &String| -> u32 {
            // Convert usize to u32, clamping on overflow
            value.len().try_into().unwrap_or(u32::MAX)
        })
        .max_capacity(32 * 1024 * 1024) // 32 MiB budget
        .build();

    cache.insert(0, "zero".to_string());
    cache.insert(1, "a".repeat(10_000)); // Consumes 10 KB of weight
}

Asynchronous Caches

The moka::future::Cache API mirrors the synchronous version. The weigher works identically across async boundaries:

use moka::future::Cache;

#[tokio::main]
async fn main() {
    let cache = Cache::builder()
        .weigher(|_key, value: &Vec<u8>| -> u32 {
            value.len().try_into().unwrap_or(u32::MAX)
        })
        .max_capacity(64 * 1024 * 1024) // 64 MiB
        .build();

    cache.insert(42, vec![0u8; 1_024 * 1024]).await; // 1 MiB entry
}

Custom Weight Logic

For complex domain objects, combine multiple factors to calculate weight:

#[derive(Clone)]
struct Document {
    text: String,
    images: usize,
}

use moka::sync::Cache;

let cache = Cache::builder()
    .weigher(|_key, doc: &Document| -> u32 {
        let text_weight = doc.text.len() as u32;
        let image_weight = (doc.images * 10_240) as u32; // 10 KB per image
        text_weight.saturating_add(image_weight)
    })
    .max_capacity(100 * 1024 * 1024)
    .build();

Important Semantics and Constraints

When implementing your weigher closure, adhere to these requirements defined in the Moka source:

Aspect Requirement
Return Type Must be u32. The cache stores this as policy_weight in EntryInfo.
Thread Safety Closure must implement Send + Sync + 'static because the cache invokes it from multiple threads.
Panic Safety The closure must never panic. Panics during weight calculation will abort the insertion operation.
Overflow Handling If your calculation exceeds u32, clamp to u32::MAX or use saturating_add to prevent wraparound.
Eviction Impact Both TinyLFU and LRU policies use the calculated weight; larger weights cause earlier eviction when capacity is reached.

Interaction with Other Cache Features

The weigher closure integrates with other Moka capabilities without conflict:

  • Expiration Policies – Time-to-live (TTL) and time-to-idle (TTI) operate independently of weights. An entry can expire due to time regardless of its weight.
  • Invalidation Closures – When using support_invalidation_closures(), the weight of invalidated entries is automatically subtracted from the total weighted_size.
  • Eviction Listeners – When the cache evicts an entry due to weight constraints, the eviction listener receives the key and value exactly as it would for count-based eviction.

Summary

  • The weigher closure in moka-rs/moka enables size-aware caching by converting key-value pairs into u32 weights.
  • Define the closure using Cache::builder().weigher(|k, v| ...); the implementation resides in src/sync/builder.rs (lines 414-422).
  • The closure must be Send + Sync + 'static, return u32, and never panic.
  • Internally, src/sync/base_cache.rs (lines 40-42) invokes the closure during insertion to set policy_weight in the entry metadata.
  • Use this feature to limit cache memory consumption (e.g., bytes, object complexity) rather than just entry counts.

Frequently Asked Questions

What happens if I don't specify a weigher closure?

If you do not call .weigher() on the builder, Moka defaults to a unit weight of 1 for every entry. In this mode, max_capacity limits the total number of entries rather than their cumulative weight.

Can the weigher closure capture variables from the surrounding scope?

Yes, the weigher can capture variables, but they must satisfy the 'static lifetime requirement. Avoid capturing references to short-lived data; instead, use Arc or owned data if the closure needs external state.

How does the weigher affect cache performance?

The weigher is invoked synchronously during every insert operation. Keep the closure lightweight to avoid blocking cache operations. Complex calculations (e.g., deep object traversal) should be pre-computed and stored in the value type if possible.

What should I do if my value size exceeds u32::MAX?

Clamp the value to u32::MAX using try_into().unwrap_or(u32::MAX) or saturating_add for combined calculations. While this caps individual entries at the maximum weight, it prevents overflow and ensures the cache continues to function correctly.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →