API reference¶
This reference covers the complete public surface of client_query_cache: construction, configuration, the cached
read methods, ownership rules, and how to fall back to plain PyMongo. Start with the synchronous or asyncio tutorial for a complete program.
Synchronous names live at the top level (client_query_cache) and wrap pymongo.MongoClient. The same names are available under client_query_cache.asynchronous and wrap pymongo.AsyncMongoClient. With asyncio, await every cached read except find(), which returns its async cursor immediately; await aggregate() before consuming its cursor. Cached read methods shows both forms. Everything below applies to both APIs unless it names an asyncio difference.
Constructing a manager¶
CacheManager(client, *, cache_config=None, max_await_time_ms=1_000) wraps a caller-constructed and caller-owned MongoClient (or
AsyncMongoClient). It never closes that client and never mutates it.
cache_manager.client— the wrapped PyMongo client, unchanged.cache_manager.cache_core— the manager's cache storage and bookkeeping object; see Observability.cache_manager.get_cached_collection(collection)— a cached read view of one of your PyMongo collections; see Cached collection views.cache_manager.close()(await cache_manager.close()for asyncio) — stops every change stream the manager opened and releases cached data. Does not closecache_manager.client.CacheManageris also a context manager (with/async with), callingclose()on exit.
Close the manager (or use it as a context manager) whenever you close the client it wraps. A manager starts a background change-stream task the first time a read touches a database; leaving the manager open after closing the client leaves that task running against a closed connection.
Cached collection views¶
Keep two handles for a collection: PyMongo's own collection for writes, administration, and any read you don't want cached, and a cached view of it for the six cached reads.
collection = client["my_database"]["my_collection"]
cached_collection = cache_manager.get_cached_collection(collection)
collection.insert_one({"_id": "example", "value": 42})
cached_collection.find_one({"_id": "example"})
The asyncio version is identical except that both calls are awaited:
collection = client["my_database"]["my_collection"]
cached_collection = cache_manager.get_cached_collection(collection)
await collection.insert_one({"_id": "example", "value": 42})
await cached_collection.find_one({"_id": "example"})
The cached read does not wait for the insert's invalidation: if an earlier read cached the lookup, including a missing result, find_one() can still return that result until the manager processes the write's change-stream event. See read after write.
cache_manager.get_cached_collection(collection) returns a CachedCollection whose .raw is exactly the collection you passed, including options you chose with get_collection(...) or with_options(...) — those options decide whether a read can use the cache (see Bypass conditions). The collection must come from the manager's own client; a collection from any other client raises ValueError.
Calling get_cached_collection(collection) more than once is safe and cheap: every view from one manager shares that manager's cache and its single change stream per database, so a result cached through one view is a hit through another. Each call may return a new view object, so don't rely on two views being the same object; keep one around when convenient.
cache_manager[name] returns a CachedDatabase, and database[name] returns a CachedCollection, as a shorthand for cached reads when you don't already hold a PyMongo collection. Attribute access (database.users, cached_collection.chunks) also returns a cached view of that collection or sub-collection. Index access always works, including for a collection named like a PyMongo method (database["create_collection"]).
CachedDatabase.name,CachedCollection.name— the wrapped object's name.CachedDatabase.raw,CachedCollection.raw— the wrapped PyMongoDatabase/Collection; see Raw fallback.CachedDatabase.manager— the owningCacheManager.CachedCollection.database— the owningCachedDatabase.
Cached views expose nothing else. PyMongo methods such as insert_one, create_index, drop, with_options, or create_collection raise AttributeError on a view — call them on your PyMongo object or on .raw.
Cached read methods¶
CachedCollection provides six cached reads. Eligible reads can reuse cached results; ineligible reads execute through PyMongo. Cached views remain read-only: writes and other methods belong on the PyMongo collection or .raw.
| Method | Synchronous result | Asyncio call | Cache lookup and execution |
|---|---|---|---|
find_one |
Document or None |
Awaited | During the call |
find |
Cursor subclass |
Not awaited; returns an AsyncCursor subclass |
When consumption starts |
aggregate |
CommandCursor |
Awaited; returns an AsyncCommandCursor |
During the call; a miss or bypass runs the initial command before returning |
count_documents |
int |
Awaited | During the call |
estimated_document_count |
int |
Awaited | During the call |
distinct |
list |
Awaited | During the call |
find() construction validates arguments locally and performs no database read or cache lookup.
with cached_collection.find({"status": "active"}).sort("name").limit(10) as cursor:
for product in cursor:
print(product)
items = cached_collection.find({"status": "active"}).to_list()
summary = cached_collection.aggregate([{"$match": {"status": "active"}}]).to_list()
With asyncio, use async for product in cached_collection.find(...), or await cached_collection.find(...).to_list() for a list. For aggregation, first use cursor = await cached_collection.aggregate(...), then await cursor.to_list(). Async cursor contexts use async with.
Misses stream through native batching. The cache admits only successfully consumed complete results: partial to_list(length=...), early close, errors, cancellation, or invalidation during consumption prevent incomplete or stale admission. Continue consuming a partial cursor to finish its result. Close cursors you abandon; closing a cursor never closes the client.
Find sorting, skipping, limits, and collation determine the final query and its cache identity. Clone, copy, rewind, and supported synchronous indexing use native query semantics and check current cache eligibility for their new execution. Async indexing raises the native error. Unsupported options and operations execute natively, including hints, comments, timeouts, arbitrary flags, explain(), and cursor distinct().
In both APIs, find() shares cached results across top-level permutations of
ordinary scalar equality predicates in plain dictionaries. Values must be
None or exact built-in bool, int, float, or str instances, with exact built-in string field
names not starting with $. Other options and value types remain distinct.
Document/array values, operators, regexes, BSON-specific scalars, and custom
forms retain ordered matching and their existing eligibility. Native execution
receives the original filter. Other read methods do not gain this rule. See the
scalar equality example.
A positive integer find limit can reuse an isolated prefix of a complete, valid cached query with an equal or larger positive limit, or an unlimited integer-zero/omitted limit. Every other final query input must match, including filter representation, projection, ordered sort, skip, collation, namespace, and codec options. Smaller sources cannot cover larger requests. Unlimited, negative, and boolean requests use exact lookup only; negative and boolean sources cannot cover other limits. A local find hit's retrieved counts the loaded prefix, and the cursor has no server cursor, address, or session. See the complete-consumption example and consistency limits.
Explicit find batching before execution and aggregate batchSize at invocation bypass caching. Later command-cursor batch_size() calls retain native validation and return the same cursor: they change future native getMore batching on a miss or bypass, while a local hit remains local and issues no command.
A started hit consumes one isolated snapshot, even if a write, stream interruption, eviction, or manager closure follows. Hit metadata reports cursor_id == 0, address is None, and session is None; alive reflects unread documents. A find hit's collection is the wrapped PyMongo collection, and retrieved counts documents loaded into its buffer. Hits skip server query execution, so they cannot reproduce fresh server/network errors or server-side query effects. Use .raw whenever execution itself is required.
Single-document reads¶
Both synchronous and asyncio views accept
find_one(filter=None, projection=None, *, sort=None, collation=None, session=None, **kwargs)
and return a document or None. Deterministic filters, including compound predicates, regular expressions and
match-all reads, can be cached. Repeated missing results can also hit the cache.
cached_collection.find_one(
{"status": "active", "region": "Europe"},
{"name": 1, "_id": 0},
sort=[("priority", -1), ("name", 1)],
collation={"locale": "en", "strength": 2},
)
Use await with the same arguments on an asyncio view. sort accepts a sequence of field/direction pairs,
with directions 1 or -1; collation accepts a PyMongo Collation or a dictionary. Omitting collation uses
the collection default. Sorting follows MongoDB's ordering guarantees, including its handling of ties.
Different filters, projections, sorts, collations and decoding options retain their own results.
find_one() filters that pin _id to an exact value under simple collation, alone or with additional
predicates, and qualifying unique-index find_one() reads can retain a cached result after writes to other
documents. For example, cached_collection.find_one({"_id": order_id, "customer_id": customer_id})
keeps its cached result when another order changes. Partial, sparse and hashed indexes do not qualify for this optimization.
Unique-index collation must have a confirmed match: inherited defaults and fully specified explicit matches
qualify; an explicit collation with omitted defaults may use collection-wide invalidation instead.
Other eligible single-document queries are refreshed after any write to the collection, including writes
that could introduce a match for a cached missing result. Every find(), aggregate(),
count_documents(), distinct() and estimated_document_count() result is also refreshed after any
write to the collection.
Cache invalidation is eventual. A cached read can return its previous value until the manager processes the relevant change-stream event. Use the PyMongo collection or a session-bound read when you need to immediately observe a preceding write. Additional keyword options execute directly through PyMongo; malformed arguments preserve the driver's errors.
Bypass conditions¶
A read bypasses the cache — executing as a normal PyMongo call instead of a lookup or admission — whenever caching it safely isn't possible:
- The caller supplies a session or uses
session.bind(), a read preference other than primary, or a read concern other than majority. Leaving read concern unspecified (the common case) does not bypass caching: an eligible miss reads from the primary withmajorityread concern, while an uncached read uses the deployment's default read concern, normallylocal. Majority reads usually perform comparably, but they return only writes acknowledged by a majority of replica set members. While replication lags or no majority commit point is available, a miss can therefore return older data than alocalread, or fail where alocalread would succeed. Majority reads keep writes that could later roll back out of the cache; they do not make hits current, because invalidation remains asynchronous. - Find cursor-only requests (tailable, exhaust, partial results), explicit find batching, unsupported find options, and aggregation batching supplied at invocation execute natively.
$changeStreampipelines also execute natively without caching. - Pipelines that use MongoDB Search or Vector Search, or list search indexes (
$search,$searchMeta,$vectorSearch,$listSearchIndexes), run through PyMongo with the caller's read concern and are never cached. - The collection is a MongoDB view.
- An aggregation pipeline joins another collection, writes, reports live statistics, or is otherwise
nondeterministic (for example a
$samplestage or a$randexpression). - A
find_one,find,count_documents, ordistinctfilter is nondeterministic. - A read depends on indexes or on the caller's roles:
$textand geospatial proximity ($near,$nearSphere,$geoNear) queries, and filters, projections, or pipelines that reference$$USER_ROLES. Creating, dropping, hiding, or redefining an index, or changing a user's roles, does not invalidate cached results. - A
find_oneorfindprojection uses$meta, or computes a changing value with$rand,$function,$$NOW, or$$CLUSTER_TIME. - The collection is a time-series collection — MongoDB does not provide change streams for time-series collections, so caching bypasses unconditionally for them. If a time-series collection is later replaced with an ordinary collection, reads may keep bypassing until a new manager is created, since MongoDB supplies no notification that refreshes a collection's type once it's been determined to be time-series.
- The target collection does not yet exist. Later reads recheck its type: a missing document in an existing ordinary collection can still be cached, and a namespace later created as an ordinary collection can be cached normally from then on.
- The manager's change stream for that database can't be established at all (an unsupported MongoDB version or topology; see system requirements) or is temporarily unhealthy (see recovery behavior).
Configuration¶
Pass a CacheCoreConfig to size a manager's cache:
from client_query_cache import CacheCoreConfig, CacheManager
cache_manager = CacheManager(
client, cache_config=CacheCoreConfig(shared_budget_bytes=128 * 1024 * 1024)
)
| Field | Default | Meaning |
|---|---|---|
shared_budget_bytes |
64 MiB | Total BSON-encoded size the manager's cache may hold at once, shared across every database and collection that manager caches. |
max_entry_bytes |
1 MiB | The largest single cached value (one document, or one find/aggregate/distinct result) the cache accepts. |
lag_capture_window_config |
10 windows of 100 events | Sizes the invalidation-lag sample windows used by the stream-cost telemetry described in Observability. Most applications never need to change this; it exists for the benchmark suite documented in stream cost benchmarks. |
shared_budget_bytes and max_entry_bytes must be positive, and max_entry_bytes must not exceed
shared_budget_bytes. Equal budgets are allowed.
The lag_capture_window_config has three numeric fields: window_count and events_per_window must be
positive; min_separation_events must be nonnegative. The default separation is zero, allowing adjacent windows.
All five numeric fields require exact built-in integers. Booleans, integer subclasses, floats (including NaN and
infinities), strings, None, and other types raise CacheConfigurationError at configuration construction,
naming the invalid field. Values are not converted. Invalid ranges or budget relationships also raise
CacheConfigurationError at construction.
Change-stream await time¶
Set max_await_time_ms directly on either manager to choose the maximum idle server wait for a change-stream batch:
The default is 1,000 ms. Each manager keeps its own setting across its databases and reconnects. Values must be
integers from 1 through 2,147,483,647; booleans and other invalid values raise CacheConfigurationError at
construction.
See the await-time measurements for the default's selection rule, retained evidence, and limitations.
This bounds an idle getMore wait. Event delivery and failure detection also depend on the server, network, and
client timeouts. If you set a nonzero PyMongo timeoutMS, it must be greater than max_await_time_ms, as required
by the driver's change-stream timeout rules.
Consider socketTimeoutMS and network infrastructure idle timeouts when choosing a value; the manager leaves
your client's timeout settings unchanged.
Limits¶
- A read whose result would exceed
max_entry_bytesonce BSON-encoded is not cached; it still returns the correct result, counted as an oversized bypass rather than a hit. - Cursor candidates also cap their retained encoded payload at
max_entry_bytes; exceeding it discards the candidate while native delivery continues. Native batches, decoded hit snapshots, wrappers, and temporary encoding allocations consume additional memory. This is not a process-memory limit; see memory beyond the cache budget. - Once the cache's total stored BSON size would exceed
shared_budget_bytes, admitting a new entry evicts the least-recently-used entries to make room. Query keys and Python metadata consume additional memory outside this budget, especially for large predicates with small results; see memory beyond the cache budget for measured examples. - A read whose
_idor result depends on a value that cannot be used as a cache key — for example abson.Codevalue, or aNaN, which is never equal to itself — is executed and returned normally without being cached.
None of these limits raise an exception to the caller: exceeding one always falls back to an uncached, correct result. See deployment guidance for how to size these limits against your deployment.
Ownership¶
CacheManager constructs and exclusively owns one cache core (CacheCore) and one change-stream coordinator
(ChangeStreamCoordinator) for its own lifetime; it does not share either with another manager. CacheCore and
ChangeStreamCoordinator are also exported directly (client_query_cache.synchronous /
client_query_cache.asynchronous) for advanced use, but constructing them yourself and sharing one CacheCore
across more than one ChangeStreamCoordinator — or activating the same database from two coordinators over one
CacheCore — is unsupported.
Such sharing can make reads for a healthy database bypass the cache without an error or warning. The repository's architecture notes explain why.
Construct one CacheManager per MongoClient (or AsyncMongoClient) you want cached. Two managers wrapping the
same underlying deployment — a synchronous and an asyncio manager in the same process, or one manager per process —
each keep an independent budget and independent change-stream cursors; see
deployment guidance for the capacity consequence of that duplication.
Raw fallback¶
Every wrapped object exposes the PyMongo object underneath:
cached_collection.raw— the wrappedpymongo.Collection(or its asyncio equivalent).database.raw— the wrappedpymongo.Database.cache_manager.client— the wrappedpymongo.MongoClient(orAsyncMongoClient).
.raw is typed as PyMongo's own class, so your editor and type checker see PyMongo's real signatures for every call made through it. Use it for writes and administration when you only hold a view, and for PyMongo's own semantics of the six cached method names — reads that must execute against MongoDB even when a cached result exists. Calls through .raw never use the cache.
Rollback to plain PyMongo¶
Because CacheManager wraps a client you already own rather than replacing it, removing the cache layer is a mechanical change: make each cached_collection read on the PyMongo collection instead, wrapping find() and aggregate() in list(...) where your code needs a list. Writes and administration already go through PyMongo and need no change. No data migration is needed either — the manager never alters stored documents, it only caches read results in your process's memory.
Errors¶
Import the cache exceptions from client_query_cache:
from client_query_cache import (
CacheClosedError,
CacheConfigurationError,
CacheError,
UnsupportedCacheRequestError,
)
The three cache exceptions below inherit from CacheError, which you can catch to handle them together.
The same exception classes apply to synchronous and asyncio managers.
| Exception | Raised when | What to do |
|---|---|---|
CacheConfigurationError |
A cache budget, lag-window field, the manager's max_await_time_ms, or an OpenTelemetry lag_percentiles value is invalid. |
Fix the configuration value. |
CacheClosedError |
A cached read is attempted after cache_manager.close(). |
Don't use a manager (or a view obtained from it) after closing it. |
UnsupportedCacheRequestError |
An explicit low-level cache operation receives an unsupported key value. | Use a supported key or the native collection. |
ValueError |
cache_manager.get_cached_collection(collection) receives a collection from a different client. |
Pass a collection from cache_manager.client. |
Every other unsupported or ambiguous condition — a tailable, exhaust, or partial-result find(), a $changeStream aggregation, an incompatible read preference or read concern, a session-bound
read, a nondeterministic filter or pipeline, a view, a time-series collection, an oversized result, a database whose
change stream isn't healthy or can't be established — bypasses the cache and returns a normal PyMongo result instead
of raising. See deployment guidance for stream-level failures, which
are retried internally rather than surfaced to callers at all.
Diagnostics¶
Both manager variants provide synchronous, read-only local inspection, including after close:
from client_query_cache import CacheSnapshot, StreamCostSnapshot, StreamHealthSnapshot
snapshot: CacheSnapshot = manager.snapshot()
stream_cost: StreamCostSnapshot = manager.stream_cost_snapshot("shop")
stream_health: StreamHealthSnapshot = manager.stream_health_snapshot("shop")
measured_databases = manager.active_stream_cost_databases()
Snapshots contain cumulative cache counters and capacity observations without document contents or queries.
Inspection does not activate a stream or make a database request. The advanced manager.cache_core
inspection methods remain available.
CacheSnapshot, BypassReason, BypassReasonCount, StreamCostSnapshot, StreamHealthSnapshot, and
StreamHealthStatus are importable from client_query_cache, client_query_cache.synchronous, and
client_query_cache.asynchronous. See bypass reasons and stream health
for their meaning and limitations.