When to Use Sync vs Async Moka Cache: Complete API Guide

Use the sync cache for blocking, multi-threaded code and the future cache for async runtimes like Tokio; both share identical eviction and expiration internals but expose different APIs to match your concurrency model.

Choosing between the synchronous and asynchronous variants in the moka-rs/moka crate depends entirely on your application's concurrency architecture. While both provide the same high-performance caching capabilities, they differ in API design and runtime requirements. This guide explains the architectural differences, decision criteria, and implementation details based on the actual source code.

Core Differences Between Sync and Async Moka Cache

The Moka crate provides two families of caches that wrap the same core data structures but expose fundamentally different interfaces.

API Design and Runtime Requirements

The synchronous (sync) cache provides blocking methods that return values directly. According to the source code in src/sync/cache.rs (line 571), the Cache<K,V> struct exposes methods like fn get(&self, key) -> Option<V> that block the current thread until the operation completes. This variant requires no async runtime and works in plain multi-threaded Rust code.

The asynchronous (future) cache returns Futures for every operation. Defined in src/future/cache.rs (line 633), this variant exposes async fn get(&self, key) -> Option<V>, allowing operations to be awaited without blocking the executor thread. This requires an async runtime (Tokio, async-std) and the crate feature future enabled.

Shared Internals: BaseCache Architecture

Both implementations are thin wrappers around BaseCache, a lock-free concurrent hash table that handles entry storage, eviction, and expiration policies. The synchronous wrapper in src/sync/cache.rs forwards calls directly to BaseCache, while the asynchronous wrapper in src/future/cache.rs adapts the same underlying storage to return awaitable futures. Because they share src/common/* modules for data structures, configuration options—capacity, weigher, eviction listeners, expiration policies, and custom hashers—remain identical regardless of which API you choose.

When to Choose the Synchronous (sync) Cache

Select the sync cache when your application runs in a purely synchronous context. This includes command-line tools, background daemon threads, or std::thread pools where you want to avoid pulling in an async runtime as a dependency.

The sync variant is also ideal for CPU-bound work where blocking a thread is acceptable. Since the underlying operations are lock-free and fast, the blocking API provides a simpler mental model without the overhead of futures and executors. You can clone the cache cheaply (Arc-based) across threads, as shown in src/sync/cache.rs, and share mutable cache state without additional synchronization.

When to Choose the Asynchronous (future) Cache

Use the future cache when your application already employs an async runtime such as Tokio or async-std. This is essential for async web servers (Actix-Web, Axum) or async worker processes where handlers are async fns.

The async variant allows you to await cache operations without blocking the executor's thread, which is critical when combining cache access with other async I/O like network requests or file system operations. You can compose cache calls with other futures using combinators like join! or select!. The implementation in src/future/cache.rs ensures that while the public API is async, the internal updates remain lock-free and performant.

Code Examples

Synchronous Cache with Thread Pools

This example demonstrates sharing a sync::Cache across multiple OS threads using cheap cloning:

use moka::sync::Cache;
use std::thread;

fn value(n: usize) -> String {
    format!("value {n}")
}

fn main() {
    const THREADS: usize = 8;
    const KEYS_PER_THREAD: usize = 50;

    // A cache that holds up to 10,000 entries.
    let cache = Cache::new(10_000);

    // Spawn threads that share the same cache via Arc-based clone.
    let handles: Vec<_> = (0..THREADS)
        .map(|i| {
            let my_cache = cache.clone(); // cheap clone
            thread::spawn(move || {
                let start = i * KEYS_PER_THREAD;
                let end = start + KEYS_PER_THREAD;
                for k in start..end {
                    my_cache.insert(k, value(k));
                    assert_eq!(my_cache.get(&k), Some(value(k)));
                }
                for k in (start..end).step_by(5) {
                    my_cache.invalidate(&k);
                }
            })
        })
        .collect();

    for h in handles {
        h.join().expect("thread panicked");
    }

    // Verify results in main thread.
    for k in 0..(THREADS * KEYS_PER_THREAD) {
        if k % 5 == 0 {
            assert!(cache.get(&k).is_none());
        } else {
            assert!(cache.get(&k).is_some());
        }
    }
}

Key implementation files:

Asynchronous Cache with Tokio

This example shows the future::Cache with TTL expiration in an async context:

use moka::future::Cache;
use std::time::Duration;
use tokio::time;

#[tokio::main]
async fn main() {
    // Cache with 30-second TTL.
    let cache = Cache::builder()
        .max_capacity(5_000)
        .time_to_live(Duration::from_secs(30))
        .build();

    // Insert asynchronously.
    cache.insert(42, "the answer".to_string()).await;

    // Concurrent access from multiple tasks.
    let handles: Vec<_> = (0..10)
        .map(|_| {
            let c = cache.clone(); // cheap clone
            tokio::spawn(async move {
                let v = c.get(&42).await;
                assert_eq!(v, Some("the answer".to_string()));
            })
        })
        .collect();

    futures_util::future::join_all(handles).await;

    // Let entry expire.
    time::sleep(Duration::from_secs(35)).await;
    cache.run_pending_tasks().await;
    assert!(cache.get(&42).await.is_none());
}

Key implementation files:

Integration with Async Web Frameworks (Axum)

The async cache is required when your surrounding code is async, such as in Axum handlers:

use axum::{
    extract::Extension,
    routing::get,
    Router,
};
use moka::future::Cache;
use std::sync::Arc;

async fn handler(Extension(cache): Extension<Arc<Cache<String, usize>>>) -> String {
    let key = "hits".to_string();
    let count = cache.get_with(key.clone(), async { 0 }).await + 1;
    cache.insert(key, count).await;
    format!("Hits: {count}")
}

#[tokio::main]
async fn main() {
    let cache = Arc::new(Cache::new(100));
    let app = Router::new()
        .route("/", get(handler))
        .layer(Extension(cache));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Key Implementation Files

File Path Role
src/sync/cache.rs (line 571) Synchronous Cache<K,V> struct implementation
src/sync/builder.rs Builder API for sync cache configuration
src/future/cache.rs (line 633) Asynchronous Cache<K,V> struct implementation (requires future feature)
src/future/builder.rs Builder API for async cache configuration
src/common/* Shared data structures and eviction logic used by both variants

Summary

  • Choose sync when working with blocking code, thread pools, or CPU-bound tasks where an async runtime is unnecessary.
  • Choose future when integrating with Tokio, async-std, or other async runtimes to avoid blocking executor threads.
  • Both variants share the same BaseCache internals, providing identical eviction, expiration, and concurrency guarantees.
  • Both support cheap Arc-based cloning to share state across threads or async tasks.
  • The future cache requires the future feature flag and an async runtime dependency.

Frequently Asked Questions

Can I use both sync and async caches in the same project?

Yes, you can import both moka::sync::Cache and moka::future::Cache in the same crate. They are independent types that share no state between them, allowing you to use the sync variant for background thread pools and the async variant for your web server within the same application.

Do sync and async caches have different performance characteristics?

No, both use the same lock-free concurrent hash table and eviction algorithms from src/common/. The only difference is the API wrapper—sync methods block the current thread while async methods return futures. The underlying cache operations have identical throughput and latency characteristics once the cache is accessed.

How do I enable the async cache features?

Add moka to your Cargo.toml with the future feature enabled: moka = { version = "0.12", features = ["future"] }. This compiles the src/future/cache.rs module and exposes moka::future::Cache. Without this feature, only the sync cache is available.

Is the sync cache thread-safe?

Yes, the sync cache is fully thread-safe and designed for concurrent access from multiple threads. It uses internal lock-free data structures, and cloning the cache (as implemented via Arc in src/sync/cache.rs) creates a new handle to the same underlying data without requiring mutexes or additional synchronization in your code.

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 →