Client-Side Caching
Client-side caching in Valkey GLIDE stores responses from cacheable read commands in a local in-memory cache on the client, reducing network round-trips and server load. When a cached command is issued again, the response is served directly from local memory without contacting the server.
How It Works
Section titled “How It Works”- When a cacheable read command is executed, GLIDE first checks the local cache.
- On a cache miss, the command is sent to the server, and the response is stored locally.
- On a cache hit, the cached value is returned immediately without a network call.
- Entries expire based on their configured TTL. Expiration is lazy — entries are removed when accessed after their TTL has elapsed, not proactively in the background.
- When the cache reaches its memory limit, entries are evicted according to the configured eviction policy.
Cached Commands
Section titled “Cached Commands”Only the following read commands are cached:
| Command | Description |
|---|---|
GET | Retrieve a string value |
HGETALL | Retrieve all fields from a hash |
SMEMBERS | Retrieve all members from a set |
All other commands bypass the cache entirely. Write commands (SET, HSET, SADD, etc.) are never cached.
What Is Not Cached
Section titled “What Is Not Cached”- NIL responses — If a key does not exist, the
nilresponse is not stored in the cache. - Entries larger than the cache — If a single entry exceeds
maxCacheKb, it is silently skipped.
Configuration
Section titled “Configuration”To enable client-side caching, pass a ClientSideCache configuration when creating a client.
from glide import ( GlideClient, GlideClientConfiguration, GlideClusterClient, GlideClusterClientConfiguration, ClientSideCache, EvictionPolicy, NodeAddress,)
# Create a cache configurationcache = ClientSideCache.create( max_cache_kb=1024, # 1 MB maximum cache size entry_ttl_ms=60_000, # 60 second TTL per entry (0 = no expiration) eviction_policy=EvictionPolicy.LRU, # LRU or LFU enable_metrics=True, # Enable hit/miss tracking)
# Standalone clientconfig = GlideClientConfiguration( addresses=[NodeAddress("localhost", 6379)], client_side_cache=cache,)client = await GlideClient.create(config)
# Cluster clientcluster_config = GlideClusterClientConfiguration( addresses=[NodeAddress("localhost", 6379)], client_side_cache=cache,)cluster_client = await GlideClusterClient.create(cluster_config)import glide.api.GlideClient;import glide.api.GlideClusterClient;import glide.api.models.configuration.GlideClientConfiguration;import glide.api.models.configuration.GlideClusterClientConfiguration;import glide.api.models.ClientSideCache;import glide.api.models.EvictionPolicy;import glide.api.models.configuration.NodeAddress;
// Create a cache configurationClientSideCache cache = ClientSideCache.builder() .maxCacheKb(1024) // 1 MB maximum cache size .entryTtlMs(60_000) // 60 second TTL per entry (0 = no expiration) .evictionPolicy(EvictionPolicy.LRU) // LRU or LFU .enableMetrics(true) // Enable hit/miss tracking // .serverAssisted(true) // Enable server-assisted invalidation (requires RESP3) .build();
// Standalone clientGlideClientConfiguration config = GlideClientConfiguration.builder() .address(NodeAddress.builder().host("localhost").port(6379).build()) .clientSideCache(cache) .build();GlideClient client = GlideClient.createClient(config).get();
// Cluster clientGlideClusterClientConfiguration clusterConfig = GlideClusterClientConfiguration.builder() .address(NodeAddress.builder().host("localhost").port(6379).build()) .clientSideCache(cache) .build();GlideClusterClient clusterClient = GlideClusterClient.createClient(clusterConfig).get();import { GlideClient, GlideClusterClient, ClientSideCache, EvictionPolicy,} from "@valkey/valkey-glide";
// Create a cache configurationconst cache = ClientSideCache.create( 1024, // maxCacheKb: 1 MB maximum cache size 60000, // entryTtlMs: 60 second TTL (0 = no expiration) { evictionPolicy: EvictionPolicy.LRU, // LRU or LFU enableMetrics: true, // Enable hit/miss tracking });
// Standalone clientconst client = await GlideClient.createClient({ addresses: [{ host: "localhost", port: 6379 }], clientSideCache: cache,});
// Cluster clientconst clusterClient = await GlideClusterClient.createClient({ addresses: [{ host: "localhost", port: 6379 }], clientSideCache: cache,});import ( "github.com/valkey-io/valkey-glide/go/v2/config" glide "github.com/valkey-io/valkey-glide/go/v2")
// Create a cache configurationcache := config.NewClientSideCache(1024, 60000). // maxCacheKb, entryTtlMs WithEvictionPolicy(config.LRU). // LRU or LFU WithMetrics(true) // Enable hit/miss tracking
// Standalone clientclientConfig := config.NewGlideClientConfiguration(). WithAddress(&config.NodeAddress{Host: "localhost", Port: 6379}). WithClientSideCache(cache)client, err := glide.NewGlideClient(clientConfig)
// Cluster clientclusterConfig := config.NewGlideClusterClientConfiguration(). WithAddress(&config.NodeAddress{Host: "localhost", Port: 6379}). WithClientSideCache(cache)clusterClient, err := glide.NewGlideClusterClient(clusterConfig)use ValkeyGlide\Cache\ClientSideCache;
// Create a cache configuration$cache = ClientSideCache::builder() ->maxCacheKb(1024) // 1 MB maximum cache size ->entryTtlMs(60000) // 60 second TTL per entry (0 = no expiration) ->evictionPolicy(ClientSideCache::EVICTION_LRU) // EVICTION_LRU or EVICTION_LFU ->enableMetrics(true) // Enable hit/miss tracking ->build();
// Standalone client$client = new ValkeyGlide();$client->connect( addresses: [['host' => 'localhost', 'port' => 6379]], client_side_cache: $cache->toArray(),);
// Cluster client$clusterClient = new ValkeyGlideCluster();$clusterClient->connect( addresses: [['host' => 'localhost', 'port' => 6379]], client_side_cache: $cache->toArray(),);using Valkey.Glide;using static Valkey.Glide.ConnectionConfiguration;
// Create a cache configurationvar cache = new ClientSideCacheConfig(maxCacheKb: 1024, entryTtl: TimeSpan.FromSeconds(60)) .WithEvictionPolicy(EvictionPolicy.LRU) // LRU or LFU .WithMetrics(true); // Enable hit/miss tracking
// Standalone clientvar config = new StandaloneClientConfigurationBuilder() .WithAddress("localhost", 6379) .WithClientSideCache(cache) .Build();var client = await GlideClient.CreateClient(config);
// Cluster clientvar clusterConfig = new ClusterClientConfigurationBuilder() .WithAddress("localhost", 6379) .WithClientSideCache(cache) .Build();var clusterClient = await GlideClusterClient.CreateClient(clusterConfig);Configuration Options
Section titled “Configuration Options”| Option | Type | Default | Description |
|---|---|---|---|
maxCacheKb | integer | — | Maximum cache size in kilobytes. Required. |
entryTtlMs | integer | — | Time-to-live per entry in milliseconds. Use 0 to disable TTL (entries persist until evicted). |
evictionPolicy | LRU or LFU | LRU | Policy for removing entries when the cache is full. |
enableMetrics | boolean | false | When true, enables collection of hit/miss/eviction/expiration counters. |
serverAssisted | boolean | false | When true, enables server-assisted invalidation using Valkey’s CLIENT TRACKING ON BCAST (broadcast) mode — the only tracking mode GLIDE currently supports. Requires RESP3. |
Eviction Policies
Section titled “Eviction Policies”When the cache reaches its configured memory limit, it must remove entries to make room for new ones.
| Policy | Name | Behavior |
|---|---|---|
LRU | Least Recently Used | Evicts the entry that has not been accessed for the longest time. Best for workloads with temporal locality. |
LFU | Least Frequently Used | Evicts the entry with the lowest access count. Ties are broken by oldest access time. Best for workloads where popular items should stay cached. |
Cache Metrics
Section titled “Cache Metrics”When enableMetrics is set to true, you can query cache performance statistics at runtime.
# Get metricshit_rate = await client.get_cache_hit_rate() # float 0.0–1.0miss_rate = await client.get_cache_miss_rate() # float 0.0–1.0entry_count = await client.get_cache_entry_count() # intevictions = await client.get_cache_evictions() # intexpirations = await client.get_cache_expirations() # inttotal = await client.get_cache_total_lookups() # int
print(f"Hit rate: {hit_rate:.2%}")print(f"Entries: {entry_count}, Evictions: {evictions}, Expirations: {expirations}")// Get metricsdouble hitRate = client.getCacheHitRate().get(); // 0.0–1.0double missRate = client.getCacheMissRate().get(); // 0.0–1.0long entryCount = client.getCacheEntryCount().get();long evictions = client.getCacheEvictions().get();long expirations = client.getCacheExpirations().get();
System.out.printf("Hit rate: %.2f%%%n", hitRate * 100);System.out.printf("Entries: %d, Evictions: %d, Expirations: %d%n", entryCount, evictions, expirations);// Get metricsconst hitRate = await client.getCacheHitRate(); // number 0.0–1.0const missRate = await client.getCacheMissRate(); // number 0.0–1.0const entryCount = await client.getCacheEntryCount(); // numberconst evictions = await client.getCacheEvictions(); // numberconst expirations = await client.getCacheExpirations(); // numberconst total = await client.getCacheTotalLookups(); // number
console.log(`Hit rate: ${(hitRate * 100).toFixed(2)}%`);console.log(`Entries: ${entryCount}, Evictions: ${evictions}, Expirations: ${expirations}`);// Get metricshitRate, err := client.GetCacheHitRate() // float64 0.0–1.0missRate, err := client.GetCacheMissRate() // float64 0.0–1.0entryCount, err := client.GetCacheEntryCount() // int64evictions, err := client.GetCacheEvictions() // int64expirations, err := client.GetCacheExpirations() // int64
fmt.Printf("Hit rate: %.2f%%\n", hitRate*100)fmt.Printf("Entries: %d, Evictions: %d, Expirations: %d\n", entryCount, evictions, expirations)// Get metrics$hitRate = $client->getCacheHitRate(); // float 0.0–1.0$missRate = $client->getCacheMissRate(); // float 0.0–1.0$entryCount = $client->getCacheEntryCount(); // int$evictions = $client->getCacheEvictions(); // int$expirations = $client->getCacheExpirations(); // int$total = $client->getCacheTotalLookups(); // int
printf("Hit rate: %.2f%%\n", $hitRate * 100);printf("Entries: %d, Evictions: %d, Expirations: %d\n", $entryCount, $evictions, $expirations);using Valkey.Glide;using static Valkey.Glide.ConnectionConfiguration;
var config = new StandaloneClientConfigurationBuilder() .WithAddress("localhost", 6379) .WithClientSideCache(new ClientSideCacheConfig(maxCacheKb: 1024, entryTtl: TimeSpan.FromSeconds(60)).WithMetrics(true)) .Build();var client = await GlideClient.CreateClient(config);
// Get metricsdouble hitRate = await client.GetCacheHitRateAsync(); // 0.0–1.0double missRate = await client.GetCacheMissRateAsync(); // 0.0–1.0long entryCount = await client.GetCacheEntryCountAsync();long evictions = await client.GetCacheEvictionsAsync();long expirations = await client.GetCacheExpirationsAsync();long total = await client.GetCacheTotalLookupsAsync();
Console.WriteLine($"Hit rate: {hitRate:P2}");Console.WriteLine($"Entries: {entryCount}, Evictions: {evictions}, Expirations: {expirations}");Shared Cache
Section titled “Shared Cache”Multiple clients can share the same cache instance by passing the same ClientSideCache object to each client. This is useful when you want several connections to benefit from a single pool of cached data.
# Both clients share the same cachecache = ClientSideCache.create(max_cache_kb=1024, entry_ttl_ms=60_000)
client1 = await GlideClient.create( GlideClientConfiguration( addresses=[NodeAddress("localhost", 6379)], client_side_cache=cache, ))client2 = await GlideClient.create( GlideClientConfiguration( addresses=[NodeAddress("localhost", 6379)], client_side_cache=cache, ))
# client1 populates the cacheawait client1.set("key", "value")await client1.get("key") # Cache miss — fetches from server
# client2 gets a cache hit without contacting the serverresult = await client2.get("key") # Cache hit// Both clients share the same cacheconst cache = ClientSideCache.create(1024, 60000);
const client1 = await GlideClient.createClient({ addresses: [{ host: "localhost", port: 6379 }], clientSideCache: cache,});const client2 = await GlideClient.createClient({ addresses: [{ host: "localhost", port: 6379 }], clientSideCache: cache,});
// client1 populates the cacheawait client1.set("key", "value");await client1.get("key"); // Cache miss — fetches from server
// client2 gets a cache hit without contacting the serverconst result = await client2.get("key"); // Cache hit// Both clients share the same cacheClientSideCache cache = ClientSideCache.builder() .maxCacheKb(1024) .entryTtlMs(60_000) .build();
GlideClient client1 = GlideClient.createClient( GlideClientConfiguration.builder() .address(NodeAddress.builder().host("localhost").port(6379).build()) .clientSideCache(cache) .build()).get();
GlideClient client2 = GlideClient.createClient( GlideClientConfiguration.builder() .address(NodeAddress.builder().host("localhost").port(6379).build()) .clientSideCache(cache) .build()).get();
// client1 populates the cacheclient1.set("key", "value").get();client1.get("key").get(); // Cache miss — fetches from server
// client2 gets a cache hit without contacting the serverString result = client2.get("key").get(); // Cache hit// Both clients share the same cachecache := config.NewClientSideCache(1024, 60000)
client1Config := config.NewGlideClientConfiguration(). WithAddress(&config.NodeAddress{Host: "localhost", Port: 6379}). WithClientSideCache(cache)client1, _ := glide.NewGlideClient(client1Config)
client2Config := config.NewGlideClientConfiguration(). WithAddress(&config.NodeAddress{Host: "localhost", Port: 6379}). WithClientSideCache(cache)client2, _ := glide.NewGlideClient(client2Config)
// client1 populates the cacheclient1.Set("key", "value")client1.Get("key") // Cache miss — fetches from server
// client2 gets a cache hit without contacting the serverresult, _ := client2.Get("key") // Cache hit// Both clients share the same cache$cache = ClientSideCache::builder() ->maxCacheKb(1024) ->entryTtlMs(60000) ->build();
$client1 = new ValkeyGlide();$client1->connect( addresses: [['host' => 'localhost', 'port' => 6379]], client_side_cache: $cache->toArray(),);
$client2 = new ValkeyGlide();$client2->connect( addresses: [['host' => 'localhost', 'port' => 6379]], client_side_cache: $cache->toArray(),);
// client1 populates the cache$client1->set('key', 'value');$client1->get('key'); // Cache miss — fetches from server
// client2 gets a cache hit without contacting the server$result = $client2->get('key'); // Cache hitusing Valkey.Glide;using static Valkey.Glide.ConnectionConfiguration;
// Both clients share the same cachevar cache = new ClientSideCacheConfig(maxCacheKb: 1024, entryTtl: TimeSpan.FromSeconds(60));
var config1 = new StandaloneClientConfigurationBuilder() .WithAddress("localhost", 6379) .WithClientSideCache(cache) .Build();var client1 = await GlideClient.CreateClient(config1);
var config2 = new StandaloneClientConfigurationBuilder() .WithAddress("localhost", 6379) .WithClientSideCache(cache) .Build();var client2 = await GlideClient.CreateClient(config2);
// client1 populates the cacheawait client1.SetAsync("key", "value");await client1.GetAsync("key"); // Cache miss — fetches from server
// client2 gets a cache hit without contacting the servervar result = await client2.GetAsync("key"); // Cache hitServer-Assisted Invalidation
Section titled “Server-Assisted Invalidation”Server-assisted invalidation can be enabled in the client side configuration by setting server assisted to true.
You do not need to call CLIENT TRACKING yourself; GLIDE issues CLIENT TRACKING ON BCAST on your behalf during connection setup.
Once enabled, the client receives push invalidation messages from the server whenever a cached key is modified, eliminating the staleness window inherent in TTL-only mode — the server notifies the client immediately when data changes, and the client removes the stale entry from its local cache.
Enabling server-assisted mode
Section titled “Enabling server-assisted mode”import glide.api.GlideClient;import glide.api.models.configuration.GlideClientConfiguration;import glide.api.models.ClientSideCache;import glide.api.models.configuration.NodeAddress;
ClientSideCache cache = ClientSideCache.builder() .maxCacheKb(1024) .entryTtlMs(60_000) .serverAssisted(true) // Enable server-assisted invalidation .build();
GlideClientConfiguration config = GlideClientConfiguration.builder() .address(NodeAddress.builder().host("localhost").port(6379).build()) .clientSideCache(cache) .build();GlideClient client = GlideClient.createClient(config).get();Server-assisted invalidation is not yet available in the Python client. Progress is tracked in issue #5963.
Server-assisted invalidation is not yet available in the Node.js client. Progress is tracked in issue #5964.
Server-assisted invalidation is not yet available in the Go client. Progress is tracked in issue #5966.
Server-assisted invalidation is not yet available in the PHP client. Progress is tracked in issue #6292.
Server-assisted invalidation is not yet available in the C# client. Progress is tracked in issue #6010.
Diagnosing tracking state with CLIENT TRACKINGINFO
Section titled “Diagnosing tracking state with CLIENT TRACKINGINFO”The clientTrackingInfo() command returns the current tracking state of the connection. This is useful for verifying that server-assisted caching is active.
// Standalone clientMap<String, Object> info = client.clientTrackingInfo().get();// info contains:// "flags" -> Set<String> e.g. {"on", "noloop"}// "redirect" -> Long (-1 if not redirecting)// "prefixes" -> Object[] of monitored key prefixes
// Cluster client — routes to a random node by defaultMap<String, Object> clusterInfo = clusterClient.clientTrackingInfo().get();
// Cluster client — query all nodesMap<String, Object> allNodesInfo = clusterClient.clientTrackingInfo(Route.ALL_NODES).get();Not yet available. See issue #5963.
Not yet available. See issue #5964.
Not yet available. See issue #5966.
Not yet available. See issue #6292.
Not yet available. See issue #6010.
Limitations
Section titled “Limitations”| Limitation | Details |
|---|---|
| TTL-only expiration | No server-side invalidation by default. Cached values may become stale if the key is modified on the server before the TTL expires. Java supports server-assisted invalidation which eliminates this limitation. |
| Lazy expiration | Expired entries are cleaned up on access, not proactively in the background. |
| Limited command coverage | Only GET, HGETALL, and SMEMBERS are cached. Other read commands are not cached. |
| NIL not cached | If a key does not exist, the nil response is not stored. |
| No invalidation on writes | In TTL-only mode, writing to a key (e.g., SET, HSET, SADD, DEL) does not invalidate the local cache entry — even when the write is issued by the same client that has the value cached. Subsequent reads return the stale value until its TTL expires or it is evicted under memory pressure. With server-assisted mode (Java), the server pushes invalidation messages that clear the stale entry immediately. |
Compatibility with managed services
Section titled “Compatibility with managed services”The Java client now supports server-assisted invalidation via CLIENT TRACKING. When using this feature, the deployment must expose the CLIENT TRACKING command. The table below summarizes compatibility:
| Deployment | Exposes CLIENT TRACKING? | Notes |
|---|---|---|
| Self-managed Valkey or Redis OSS | ✅ Yes | Requires Valkey 7.2+ / Redis OSS 6.0+ (any version that supports CLIENT TRACKING). |
| Amazon MemoryDB | ✅ Yes | Standard CLIENT surface available. |
| Amazon ElastiCache (cluster-mode replication group, non-serverless) | ✅ Yes | Standard CLIENT surface available. |
| Amazon ElastiCache Serverless | ❌ No | Only CLIENT GETNAME / SETNAME / REPLY / HELP are exposed; CLIENT TRACKING returns ERR unknown subcommand 'tracking'. The TTL-only behavior described above will continue to apply on Serverless even after server-driven invalidation lands in GLIDE. |
If your deployment is in the last row, plan around the TTL-only model: cache only data that is immutable or can tolerate staleness up to its TTL.
Validating cache behavior
Section titled “Validating cache behavior”The metrics counters give you an objective way to confirm caching is doing what you expect. The following sequence is short enough to run interactively from a single client with enableMetrics(true) set, and illustrates the most common behaviors:
| Step | Operation | What you should see |
|---|---|---|
| 1 | SET k v1 | Server-only. Cache state unchanged. |
| 2 | GET k | Cache miss, populates the entry. entryCount=1, totalLookups=1, hitRate≈0. |
| 3 | GET k | Cache hit. totalLookups=2, hitRate≈0.5. |
| 4 | GET nonexistent | Cache miss; the nil reply is not cached. entryCount stays at 1, missRate rises. |
| 5 | SET k v2 | Server-only. The cache still holds v1 — no invalidation, even though the same client just wrote. |
| 6 | GET k | Returns v1 — the stale cached value. The cache catches up only after the entry’s TTL expires or the entry is evicted under memory pressure. |
Confirming the limit on the same client
Section titled “Confirming the limit on the same client”The biggest surprise for new users is that the cache does not clear after the same client writes a key it had cached. You can confirm this with a single client and four operations:
SET k "before"GET k(populates the cache).SET k "after"(writes go through to the server; the cache is untouched).GET k→ returns"before"until the entry’s TTL expires.
This is the expected behavior of the TTL-only implementation. Until server-driven invalidation lands, treat any key the application writes to as not safely cacheable, regardless of which client performs the write.
Best Practices
Section titled “Best Practices”- Set an appropriate TTL — Choose a TTL that balances freshness with cache effectiveness. Shorter TTLs reduce staleness risk; longer TTLs improve hit rates.
- Size the cache appropriately — Monitor eviction counts. High eviction rates indicate the cache is too small for the working set.
- Use metrics to tune — Enable metrics during development and load testing to understand cache behavior and optimize configuration.
- Use only for read-heavy, slow-changing data — Until server-driven invalidation ships, restrict the cache to values that can tolerate staleness up to your TTL: configuration, lookup tables, slow-changing reference data, computed projections rebuilt out-of-band, and similar. Avoid caching any key that the application itself writes to during its lifetime — reads after the write will return stale data.
- Avoid sharing caches across databases — Keys in different databases may have the same name but different values.