Skip to main content

Client API: How to Setup Aggressive Caching

  • By default, the RavenDB client caches every HTTP response and revalidates it with the server using ETags and 304 Not Modified on every read.

  • Aggressive caching skips that revalidation round trip:
    inside an aggressive caching scope, the client serves the cached response straight from local memory without contacting the server.

  • Use aggressive caching where the same documents are read many times, where reference data changes rarely, and where end-to-end latency matters more than absolute freshness.

  • Cached entries are invalidated automatically through the Changes API.
    A short notification delay can briefly cause stale reads inside the scope.

  • Some requests bypass aggressive caching by design, including queries with WaitForNonStaleResults, queries with NoCaching(), and internal cluster traffic.


Caching in RavenDB

Standard HTTP caching

Reading a document from the server costs a network round trip.
When the same document is read repeatedly, that round trip dominates the cost of the call: the database itself can usually answer in microseconds, so cutting the round trip translates into lower latency in your application, less bandwidth use, and reduced load on the server.

The RavenDB client caches every HTTP response it receives.
On the next read of the same data, the client sends only the cached ETag.
The server replies with 304 Not Modified when nothing has changed, no payload crosses the network, and the client serves the cached value to your code as if a fresh response had arrived.
The total size of this cache is bounded by the MaxHttpCacheSize convention; once the limit is reached, older entries are evicted.

This standard cache is on by default and works without any setup.
The trade-off is that every read still costs one network round trip to confirm freshness.
For workloads that read the same documents over and over, that single round trip becomes the bottleneck.


Aggressive caching

Aggressive caching skips the round trip entirely.
While a request runs inside an aggressive caching scope, the client serves the cached response straight from local memory without contacting the server.
This is the right choice when the same documents are read many times, when reference data changes rarely, and when end-to-end latency matters more than absolute freshness.

The trade-off is staleness.
Because the client does not revalidate on every read, a document that was modified on the server can briefly be served from the local cache until the change is propagated back to the client.
RavenDB narrows that window by listening for change notifications from the server and evicting matching cache entries as soon as they arrive.


How invalidation works

The first time aggressive caching is enabled for a database, the client opens a Changes API subscription to that database.
The subscription stays open for as long as the document store is alive and is shared by every aggressive scope opened against the same database.

When a document or index is modified on the server, the server publishes a change notification on that subscription.
The client treats that as a signal that the local cache may be stale and marks every aggressively-cached entry for revalidation.
The next read of each entry sends a single conditional request (If-None-Match) and is served from the cache as soon as the server confirms the entry is unchanged with 304 Not Modified.
Entries that are not read again stay marked until they are.
No application code is required for any of this; the client wires up the listener for you.

If the Changes API connection drops, the client invalidates the entire cache as a safety measure.
The next read of any aggressively cached entry is sent to the server for revalidation.

There is a short delay between the moment a document is modified on the server and the moment the change notification reaches the client.
A read that hits the aggressive cache during that window will return the previous value of the document.
Use aggressive caching only when this small staleness window is acceptable for the calling code.
For requests that must always see the most recent committed state, use the bypass options under Opt out for a single call and Requests that are never aggressively cached.

Enable aggressive caching

Aggressive caching is enabled for a scope that you define with a using block.
The configuration applies to every request issued from inside that using block, including from any code awaited from within it.
When the using block exits, the previous configuration is restored automatically.

Enable for a scope

Call AggressivelyCacheFor with a duration and wrap the code that should use the cache in a using block over the returned object:

using (documentStore.AggressivelyCacheFor(TimeSpan.FromMinutes(5)))
{
using (var session = documentStore.OpenSession())
{
// Reads in this scope are served from the local cache when the
// cached entry is younger than 5 minutes and has not been
// invalidated by a change notification from the server.
var order = session.Load<Order>("orders/1");
}
}

For asynchronous code, await AggressivelyCacheForAsync and use the returned disposable the same way:

using (await documentStore.AggressivelyCacheForAsync(TimeSpan.FromMinutes(5)))
{
using (var session = documentStore.OpenAsyncSession())
{
var order = await session.LoadAsync<Order>("orders/1");
}
}

The cacheDuration value is the maximum age allowed for a cached entry.
While the entry is younger than this value, the client serves it locally.
Once it ages past the value, or when a change notification invalidates it earlier, the client fetches a fresh copy from the server on the next read.

The same rules apply to queries that run inside the scope.

The right Duration depends on how stale the calling code can tolerate the data being if the change notification is delayed or missed.
Reference data that changes rarely is a good fit for hours or days; hot reads where you still want some safety net work well at minutes; when the calling code must always see the latest committed value, do not use aggressive caching at all and rely on the standard 304 cache instead.


Use convention defaults

If most of your aggressive caching scopes use the same duration and mode, set them once on the document store and call AggressivelyCache() (no arguments) instead.
The no-argument call reads the duration and mode from Conventions.AggressiveCache.

var documentStore = new DocumentStore
{
Urls = new[] { "http://localhost:8080" },
Database = "Northwind",
Conventions =
{
AggressiveCache =
{
Duration = TimeSpan.FromMinutes(5),
Mode = AggressiveCacheMode.TrackChanges
}
}
};
documentStore.Initialize();

using (documentStore.AggressivelyCache())
{
// Uses Conventions.AggressiveCache.Duration and .Mode.
}

If you do not configure these conventions, the client falls back to factory defaults: a duration of one day and AggressiveCacheMode.TrackChanges.
Code that calls AggressivelyCache() keeps working when you later override the defaults at the document-store level; only the values it picks up change.

Where each value comes from depends on which form of the call you use:

CallDurationMode
AggressivelyCache()from Conventions.AggressiveCache.Durationfrom Conventions.AggressiveCache.Mode
AggressivelyCacheFor(duration)the value you passfrom Conventions.AggressiveCache.Mode
AggressivelyCacheFor(duration, mode)the value you passthe value you pass

Apply to the entire application

If aggressive caching is the right policy for the whole application, open one scope at startup and call Dispose() on the returned object during shutdown:

var aggressiveScope = documentStore.AggressivelyCacheFor(TimeSpan.FromMinutes(5));

// ... the application runs and serves requests ...

aggressiveScope.Dispose(); // typically called when the application shuts down

The mechanism is identical to a short-lived using block; only the moment of disposal differs.
There is no separate "global" mode on the document store.

Cache modes

The AggressiveCacheMode enum controls whether the client subscribes to the Changes API to invalidate cached entries within an aggressive scope.

TrackChanges

This is the default value.
The client opens a Changes API connection to the database and listens for notifications about modified documents and indexes.
A cache entry is evicted as soon as the matching notification arrives.
A request inside the aggressive scope serves a cached value only when the entry has not been flagged as modified.

This is the right mode for most applications: aggressive caching skips the round trip on stable data, and changes still propagate to the client within the notification delay.


DoNotTrackChanges

The client does not open a Changes API connection.
Cached entries are served for the entire Duration of the scope without any revalidation, no matter what happens on the server.

Use this mode when you explicitly accept stale reads in exchange for not maintaining a Changes API connection.
For example, when the duration is short enough that staleness is bounded by it, or when the client is running in an environment where the Changes WebSocket is undesirable.

using (documentStore.AggressivelyCacheFor(TimeSpan.FromMinutes(5),
AggressiveCacheMode.DoNotTrackChanges))
{
// Reads here are served from the cache for up to 5 minutes
// and ignore any changes published by the server.
}

More operations

Opt out for a single call

When your code runs inside an aggressive scope opened higher up the call stack and one specific read needs to reach the server anyway (for example, right after a write you just performed), wrap that read in a using block over DisableAggressiveCaching().
The disposable restores the enclosing aggressive configuration when the inner scope ends.

using (documentStore.AggressivelyCacheFor(TimeSpan.FromMinutes(5)))
{
using (var session = documentStore.OpenSession())
{
var cached = session.Load<Order>("orders/1"); // may be served from the cache

using (documentStore.DisableAggressiveCaching())
{
var fresh = session.Load<Order>("orders/1"); // forced server round trip
}
}
}

DisableAggressiveCaching() only suspends aggressive caching.
The standard 304-based HTTP cache still runs.
To disable HTTP caching as well, see Disable Caching per Session and the MaxHttpCacheSize convention.


Requests that are never aggressively cached

Some requests bypass aggressive caching regardless of the active scope:

RequestAggressively cached?Notes
session.Load, session.LoadAsync, session.Include, etc.YesDefault behavior.
Query without WaitForNonStaleResultsYesDefault behavior.
Query with WaitForNonStaleResultsNoAggressive caching is suppressed because the caller explicitly asked for non-stale data.
Query with NoCaching()NoCaching is disabled for the request entirely, both standard and aggressive.
Lazy query, lazy suggestion querySame rule as the non-lazy formLazy operations are bundled into a single MultiGet request. Aggressive caching can only short-circuit the batch when every operation in it is aggressively cacheable; if any one operation has WaitForNonStaleResults or NoCaching() set, the entire batch is sent to the server (individual operations can still be served from the standard 304 cache on the response path). See Perform queries lazily.
Internal cluster bookkeepingNoTopology and similar maintenance traffic is never aggressively cached.

A session opened with SessionOptions.NoCaching = true disables HTTP caching for every request that session issues, including aggressive caching.
See Disable Caching per Session.


Common mistakes

  • Forgetting to dispose the returned object.
    The object returned by AggressivelyCacheFor controls the lifetime of the scope.
    Without a using block, or an explicit Dispose() call, the scope stays attached to the current execution context (AsyncLocal-based) and is inherited by anything that runs from the same async chain, including subsequent requests in long-lived hosts.

  • Expecting the scope to be tied to a specific IDocumentSession.
    The configuration lives on the request executor, not on the session.
    The scope is propagated through async/await, so a session opened inside the using block inherits it.
    A session opened outside the using block does not.

  • Expecting WaitForNonStaleResults to honor the cache.
    Queries with WaitForNonStaleResults always reach the server and never read from the aggressive cache.

  • Treating Duration as a refresh interval.
    Duration is the maximum age allowed for a cached entry.
    In TrackChanges mode, an entry can also be evicted earlier when the server publishes a change notification.

Syntax

Methods

Open an aggressive caching scope for the calling code.

IDisposable AggressivelyCacheFor(TimeSpan cacheDuration, string database = null);

IDisposable AggressivelyCacheFor(TimeSpan cacheDuration,
AggressiveCacheMode mode,
string database = null);

ValueTask<IDisposable> AggressivelyCacheForAsync(TimeSpan cacheDuration,
string database = null);

ValueTask<IDisposable> AggressivelyCacheForAsync(TimeSpan cacheDuration,
AggressiveCacheMode mode,
string database = null);

Usage:

using (documentStore.AggressivelyCacheFor(TimeSpan.FromMinutes(5),
AggressiveCacheMode.TrackChanges))
{
using (var session = documentStore.OpenSession())
{
var order = session.Load<Order>("orders/1");
}
}

ParameterTypeDescription
cacheDurationTimeSpanMaximum age allowed for a cached entry to be served from the local cache without contacting the server.
modeAggressiveCacheModeWhether to track server-side changes for cache invalidation. When omitted, the value of Conventions.AggressiveCache.Mode is used.
databasestringThe database the scope applies to. When null, the document store's default database is used.
Return value
IDisposable / ValueTask<IDisposable>An object whose disposal restores the previous aggressive caching configuration.

Classes

public sealed class AggressiveCacheOptions
{
public AggressiveCacheOptions(TimeSpan duration, AggressiveCacheMode mode);

public TimeSpan Duration { get; set; }

public AggressiveCacheMode Mode { get; set; }
}

PropertyTypeDescription
DurationTimeSpanMaximum age allowed for a cached entry to be served without contacting the server.
ModeAggressiveCacheModeWhether the client tracks server-side changes for cache invalidation.

In this article