How to Use Moka Sync Cache in Rust: Thread-Safe Memory Caching
The moka sync cache provides a high-performance, thread-safe in-memory cache for Rust that uses lock-free data structures to enable safe concurrent access without requiring external synchronization primitives like Mutex or RwLock.
The moka sync cache is the synchronous API of the moka-rs/moka crate, a popular high-performance caching library for Rust. Unlike the asynchronous variant, the synchronous cache lives under moka::sync and is built on top of a lock-free hash table, making all operations safe to call from multiple threads without additional synchronization overhead. This implementation is ideal for CPU-bound workloads and traditional multi-threaded applications where you need deterministic, blocking cache operations.
Creating a Moka Sync Cache
You can instantiate a cache using either the default constructor for quick setup or the builder API for fine-grained control over capacity, expiration, and eviction policies.
Using Cache::new for Default Configuration
The fastest way to create a cache is with Cache::new(max_capacity), which allocates a lock-free hash table with default settings. According to the implementation in [src/sync/cache.rs](https://github.com/moka-rs/moka/blob/main/src/sync/cache.rs), this method constructs a BaseCache internally and wraps it with the public Cache API.
use moka::sync::Cache;
// Create a cache holding up to 10,000 entries
let cache = Cache::new(10_000);
Using CacheBuilder for Custom Policies
For production workloads requiring expiration policies or custom weigher functions, use CacheBuilder defined in [src/sync/builder.rs](https://github.com/moka-rs/moka/blob/main/src/sync/builder.rs). The builder validates expiration limits during construction to prevent invalid configurations.
use moka::sync::Cache;
use std::time::Duration;
let cache = Cache::builder()
.max_capacity(5_000)
.time_to_live(Duration::from_secs(60)) // TTL: 60 seconds
.time_to_idle(Duration::from_secs(10)) // TTI: 10 seconds
.build();
Core Cache Operations
The Cache struct in src/sync/cache.rs provides standard operations for inserting, retrieving, and removing entries. All methods are internally synchronized using atomic operations, so you must not wrap the cache in a Mutex or RwLock.
Inserting and Retrieving Entries
Use insert to store key-value pairs and get to retrieve them. The get method returns Option<V> and performs O(1) lookups on the lock-free hash table.
// Insert a value
cache.insert("key", "value");
// Retrieve a value
if let Some(value) = cache.get(&"key") {
println!("Found: {}", value);
}
Removing Entries with invalidate
To remove a specific entry, use invalidate, which immediately removes the key from the internal hash table and schedules any associated eviction listener callbacks.
cache.invalidate(&"key");
Lazy Initialization with get_with
The moka sync cache prevents the thundering herd problem through the get_with family of methods. As implemented in [src/sync/cache.rs](https://github.com/moka-rs/moka/blob/main/src/sync/cache.rs) (lines 46-52), these methods guarantee that concurrent calls for the same missing key coalesce into a single evaluation of the closure, even under high contention.
use moka::sync::Cache;
use std::sync::Arc;
let cache: Cache<usize, Arc<Vec<u8>>> = Cache::new(100);
// Expensive computation runs only once per key
let data = cache.get_with(1, || {
println!("Computing expensive value...");
Arc::new(vec![0u8; 10 * 1024 * 1024]) // 10 MiB
});
// Subsequent calls return the cached Arc without re-running the closure
let data2 = cache.get_with(1, || unreachable!());
assert!(Arc::ptr_eq(&data, &data2));
For fallible operations, use try_get_with for Result types or optionally_get_with for Option types.
Configuring Expiration and Eviction
The cache supports automatic expiration through the Housekeeper background task (defined in [src/common/housekeeper.rs](https://github.com/moka-rs/moka/blob/main/src/common/housekeeper.rs)) which periodically scans for expired entries using a timer wheel.
Setting Time-to-Live and Time-to-Idle
Configure expiration policies via the builder:
- Time-to-Live (TTL): Maximum duration an entry remains in the cache regardless of access.
- Time-to-Idle (TTI): Duration after which an entry expires if not accessed.
use moka::sync::Cache;
use std::time::Duration;
let cache = Cache::builder()
.time_to_live(Duration::from_secs(3600)) // Entries live 1 hour max
.time_to_idle(Duration::from_secs(300)) // Expire if idle 5 minutes
.build();
Adding an Eviction Listener
Register a closure to be called when entries are evicted or expired. The listener receives the key, value, and eviction cause.
let listener = |key: &u32, value: &String, cause| {
println!("Evicted {} => {} because {:?}", key, value, cause);
};
let cache = Cache::builder()
.max_capacity(100)
.eviction_listener(listener)
.build();
Thread Safety and Housekeeping
The moka sync cache is designed for extreme concurrency using a lock-free core in [src/sync/base_cache.rs](https://github.com/moka-rs/moka/blob/main/src/sync/base_cache.rs).
Cheap Clone Operations
The Cache struct is Clone, but cloning does not duplicate the underlying data. According to [src/sync.rs](https://github.com/moka-rs/moka/blob/main/src/sync.rs) (lines 30-38), cloning creates another Arc pointer to the same internal tables, making the operation O(1) and ideal for sharing across threads.
use std::thread;
let cache = Cache::new(1_000);
let handles: Vec<_> = (0..4).map(|i| {
let thread_cache = cache.clone(); // Cheap O(1) clone
thread::spawn(move || {
thread_cache.insert(i, format!("thread-{}", i));
})
}).collect();
for h in handles { h.join().unwrap(); }
Forcing Immediate Cleanup with sync()
By default, expired entry removal and eviction listener callbacks run in a background thread. For deterministic state in tests or shutdown sequences, call sync() from the ConcurrentCacheExt trait (exposed in [src/sync.rs](https://github.com/moka-rs/moka/blob/main/src/sync.rs), lines 30-34) to force pending maintenance tasks to run synchronously on the current thread.
use moka::sync::Cache;
use std::time::Duration;
let cache = Cache::builder()
.max_capacity(2)
.time_to_live(Duration::from_secs(1))
.build();
cache.insert(1, "a");
std::thread::sleep(Duration::from_secs(2));
cache.sync(); // Forces expiration cleanup immediately
assert_eq!(cache.entry_count(), 0);
Alternatively, use cache.run_pending_tasks() for the same effect.
Summary
- The moka sync cache provides lock-free, thread-safe caching under
moka::syncwithout requiring external locks. - Clone freely: Sharing across threads costs O(1) because
CacheusesArcinternally. - Use
get_withto prevent duplicate expensive computations during concurrent cache misses. - Configure expiration via
CacheBuilderwith TTL and TTI policies enforced by a backgroundHousekeeper. - Force synchronization using
sync()orrun_pending_tasks()when you need deterministic state for testing or shutdown.
Frequently Asked Questions
Is the moka sync cache thread-safe?
Yes. All operations are internally synchronized via atomic operations on a lock-free hash table implemented in src/sync/base_cache.rs. You must not wrap a Cache in Mutex or RwLock; doing so would hurt performance without adding safety.
When should I use sync() versus run_pending_tasks()?
Both methods force the background housekeeper to run immediately. According to src/sync.rs, sync() is the trait method from ConcurrentCacheExt while run_pending_tasks() is the concrete implementation. They are functionally equivalent; use either when you need expired entries removed deterministically, such as in unit tests or before checking entry_count().
How does get_with prevent duplicate computations?
The get_with method uses a ValueInitializer (internal to src/sync/cache.rs) to ensure that when multiple threads simultaneously request a missing key, only one thread executes the initialization closure while others block until the value is ready. This prevents cache stampedes or "thundering herd" problems on expensive operations.
What is the difference between moka sync and async caches?
The sync cache (moka::sync) uses blocking operations and is ideal for synchronous, multi-threaded applications. The async cache (moka::future) provides the same API but returns Future types, making it suitable for async/await contexts like Tokio runtimes. Both share the same lock-free core but differ in their blocking behavior and thread pool integration.
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 →