All notable changes to this project are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
This file records what changed for users of the library, in particular
behaviour that changed under an unchanged API. The pull request that earns an
entry adds it as a fragment in changelog.d/, and the release merges the
fragments in here, so ## [Unreleased] is usually empty between releases. Each
entry opens with a bold one-line summary: - **What changed.** The details....
The GitHub release notes list those summaries and link back here, and a release
fails if there is nothing to release or an entry has no summary. Entries before
0.3.8 predate this format. See
Changelog fragments
and Releasing.
Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
0.3.8 - 2026-09-27
-
CacheLock, a distributed lock built on backend primitives. Usable as an async context manager or with directacquire/release/extend/lockedcalls, raisingLockTimeoutErroron timeout. (#64) -
expire_if_equals()backend primitive for owner-checked TTL renewal. Added toBaseCacheBackend,MemoryBackend,AsyncRedisCacheBackend, andMemcachedBackend. (#64) -
memcachedextra forMemcachedBackend. Install withfastapi-cachex[memcached], matching the backend's name. It pulls in the samepymemcachedependency as the oldmemcacheextra. (#201) -
MemoryBackend.aclose()stops the cleanup task and waits for it.stop_cleanup()only requests cancellation and stays as it is. (#181) -
ProxyNotSetErrorfor an unset manager proxy.CacheManagerProxy,SessionManagerProxyandStateManagerProxyraise it fromget()when no instance is set, instead ofBackendNotFoundError, whose name points at a backend that is not involved. It subclassesBackendNotFoundError, so existing handlers keep catching it.BackendProxyis unchanged. (#161) -
CacheXError,BackendNotFoundErrorandRequestNotFoundErrorare exported fromfastapi_cachex. They were only importable fromfastapi_cachex.exceptions, unlike every session and state exception. (#160) -
require_user_session/AuthenticatedSessionrequire a logged-in user.get_session,RequiredSessionandUserSessionDepaccept the anonymous session any visitor gets by writing torequest.session; the new dependency also answers401whensession.userisNone.UserSessionDepkeeps its behaviour until 0.4.0. (#114) -
/cached-recordsreports each entry'smedia_type. It is the media type the response was stored with, ornullwhen it had none.content_typeis still returned for compatibility but is always"bytes".CACHE_KEY_MAX_PARTSinfastapi_cachex.routesis renamedCACHE_KEY_MAX_SPLIT, since it is amaxsplitcount; the old name remains as an alias. (#184)
- GitHub release notes list one line per change. Each changelog entry now opens with a bold one-line summary. The release page shows only those summaries with their issue links, grouped as in the changelog, and links to the full entries on the documentation site. The changelog itself keeps the details.
SessionErrorderives fromCacheXError. It derived fromException, whileStateErroralready derived fromCacheXError, soexcept CacheXErrorcaught state errors but not session errors. Handlers forSessionErrororExceptionkeep working. Atryblock that listsexcept CacheXErrorbeforeexcept SessionErrornow takes theCacheXErrorbranch for session errors. (#162)@cacheserves uncached responses when the backend fails. A backend error on read or write turned every cached route into a 500, even after the handler had produced a good response; this includes a healthy Memcached rejecting a response over its 1 MB item size. A failed read now counts as a miss and a failed write leaves the response unstored, each logged as a warning onfastapi_cachex.cache.@cache(fail_open=False)restores the old behaviour.invalidate(),CacheManager,StateManager,CacheLockand sessions still raise. (#228)- Redis clear operations delete page by page.
clear(),clear_pattern()andclear_path()delete each SCAN page as it arrives instead of first collecting every matching key in memory, andclear_path()no longer runs an extraEXISTSon the direct key. (#172) - Redis
get_cache_data()no longer blocks the server. It fetched every value and TTL inside oneMULTI/EXEC, because redis-py pipelines are transactional by default. It now sends non-transactional pipelines of 100 keys each. (#171) - Memcached runs each operation in one worker call, and
delete_many()counts what it removed.increment,get_and_deleteanddelete_if_equalsused to hand every round trip to its own worker thread;delete_many()took one per key and returned how many keys it was given. Each call now makes a single trip, anddelete_many()returns how many of the keys existed. (#174, #176) CacheBackendfalls back to aMemoryBackendlike@cacheandAppCache. It used to answer 500 (BackendNotFoundError) until some@cacheroute had registered the fallback. The three lazy defaults (get_backend_or_fallback,get_app_cache,get_state_manager) now shareProxyBase.get_or_create(factory), which runs the factory at most once under a per-class lock;get_state_managercould previously register two managers under concurrent first requests. States still have no memory fallback. (#112)@cachebuilds the cache key only when it reads or writes the backend. A customkey_builderis no longer called forno_store,privateor TTL-less routes, where the key only fed debug logs. (#182)- Session lookups no longer write to the backend unless sliding expiration
renewed the session.
SessionManager.get_session()used to save the session on every call to recordlast_accessed, so each authenticated request cost a write (two if it also modified the session). The storedlast_accessedis now updated only when the session is written (created, modified, renewed or regenerated); passget_session(..., touch=True)to save it on every lookup. Session entries also store a constant fingerprint instead of hashing the payload. (#115) - Runtime dependencies declare minimum versions.
fastapi>=0.133.0,starlette>=1.0.0(now declared directly; the session middleware needs Starlette 1.0),pydantic>=2.7.0anditsdangerous>=1.1.0; the extras requirepymemcache>=4.0.0andorjson>=3.4.7. Older versions could be installed before but failed at import. Alowesttox env and CI workflow test every floor. (#194)
- Passing the Redis key prefix in a
clear_pattern()pattern. When such a pattern clears nothing, the prefix-stripped form is still tried and emits aDeprecationWarningif it clears anything. The retry will be removed in 0.4.0. (#125) - The
memcacheextra. Usememcachedinstead. The old name keeps working until 0.4.0 removes it; after that, pip and uv only warn about the unknown extra and install withoutpymemcache. (#202) fastapi_cachex.exceptions.CacheError. Nothing in the package raises it. Accessing or importing it emits aDeprecationWarning; catchCacheXErrorinstead. It will be removed in 0.4.0. (#163)
-
Redis
clear_pattern()no longer strips a pattern that starts with the key prefix. The pattern now always matches the logical key, as on the memory backend. Withkey_prefix="cache:"and the defaultCacheManager, whose keys also start withcache:,clear_pattern("user:*")used to clear nothing. (#109) -
The Redis backend warns when
encodingis not UTF-8. Entries are always written as UTF-8 JSON, but the client decoded replies with the configured encoding, so underencoding="latin-1"non-ASCII content came back corrupted without any error.AsyncRedisCacheBackendandload_from_config()now emit aRuntimeWarningfor any encoding other than UTF-8; the parameter is removed in 0.4.0 (#126). (#122) -
Counters are recognised the same way on every backend. On the memory backend and the base-class fallback,
increment()on a cached response whose body was a number, such as42, returned 43 and overwrote the response. It now raisesCacheXError, as Redis and Memcached already did. A counter written withset(key, counter_entry(n))can now be incremented on Redis and Memcached too, because it is stored as a bare integer. Stored values such as" 7"or"1_0", whichint()accepts, are no longer read as counters. (#111) -
@cachewithout a positivettlno longer stores responses or answers 304 from a stored ETag. Withttl=Noneorttl=0, the response was stored without expiry, and a request whoseIf-None-Matchmatched the stored ETag got a 304 without the handler running. After the data changed, a client revalidating with the old ETag kept getting 304 until another request rewrote the entry, and entries for every query string accumulated. These routes now skip the backend likeprivate=Trueones: the handler runs on every request, and a 304 is sent only whenIf-None-Matchmatches the freshly rendered response. Entries that earlier versions stored for them are no longer read;clear()removes them. (#110) -
The session middleware reads
Authorization: bearer <token>in any letter case. Authentication scheme names are case-insensitive (RFC 9110 §11.1), but only the exactBearerprefix was recognised, so a client sendingbearergot no session. More than one space before the token is accepted too, and a header without a token no longer yields an empty one. (#166) -
Memcached
clear_path()no longer reports a connection failure as "nothing to clear". It caught every exception and returned0, whiledelete()and the other methods let the error through. (#177) -
MemoryBackendrejects acleanup_intervalthat is not positive. With0or a negative value,asyncio.sleep()returned at once and the cleanup loop spun, using a full CPU core and taking the cache lock on every pass. It now raisesValueError. (#180) -
Redis
get_all_keys()no longer lists a key twice. SCAN may return a key more than once when the keyspace shrinks during the iteration, and the duplicates were passed through toget_all_keys(),CacheManagerand the monitoring routes. Scanned keys are now deduplicated. (#173) -
MemoryBackendno longer lists or counts expired entries. Entries that had expired but not yet been swept showed up inget_all_keys()andget_cache_data(), andclear_pattern(),clear_path()anddelete_many()counted them as removed. Redis never returns an expired key, soCacheManager.clear_prefix()and the monitoring routes reported different numbers for the same live keys depending on the backend. The expired entries are still removed; they are just not reported. As on Redis, the monitoring routes'expired_*counts now stay at zero, apart from an entry that expires while the route runs. (#178) -
MemoryBackend.clear_pattern()is case-sensitive on Windows. It usedfnmatch.fnmatch, which folds case throughos.path.normcaseon Windows, socache:user:*also clearedcache:User:1. It now usesfnmatch.fnmatchcase, like Redis on every platform. The backends docs list where the glob syntax still differs from Redis. (#179) -
Sessions no longer outlive
absolute_timeout. A sliding renewal, or asession_ttllonger thanabsolute_timeout, setexpires_at, and with it the backend TTL and the JWTexp, pastcreated_at + absolute_timeout. The expiry is now capped there, and a session whose expiry already sits at the cap is not renewed again, so it does not get a new token on every request. Because the backend now drops the record at the cap, a token presented after it raisesSessionNotFoundError, as after an ordinarysession_ttlexpiry, instead ofSessionExpiredError. (#164) -
MemoryBackendrestarts its cleanup task on a new event loop. The task stayed tied to the loop of the first cache call. If that loop was closed without cancelling it, a backend reused on another loop never cleaned up again. The task is now started again on the current loop, and a task left on a loop that is still open is cancelled there. (#181) -
MemcachedBackendraises while a server is unreachable instead of returning made-up results. pymemcache's default retries answered calls in the second after a failure with each command's default, soget()looked like a miss,set()dropped the write,increment()returned 0 anddelete_if_equals()raisedTypeError. A failed server is now taken out of rotation at once and tried again after one second, instead of after 60. (#197) -
SessionConfigwarns whencookie_same_site="none"is set withoutcookie_https_only=True. Browsers reject aSameSite=Nonecookie that is notSecure, so the session cookie was silently never stored. The combination is still accepted. (#167) -
Memcached
get_and_delete()uses CAS deletion to avoid deleting concurrent writes. The get-then-delete sequence allowed a concurrent writer to update the key between the two calls, causingget_and_delete()to delete the new value while returning the old one. It now issuesgetsand acaswrite withexptime=-1. If the key is updated beforecasruns, the operation retries to retrieve and remove the current value, matching RedisGETDEL, and raisesCacheXErrorif retries run out. (#175) -
clear_expired_sessions()also removes invalidated and expired-status sessions. It only checkedexpires_at, so a session markedINVALIDATEDbyinvalidate_session()orEXPIREDby an expired read stayed in the backend until its TTL ran out. Any session that is no longerACTIVEis now removed.clear_expired_sessions()anddelete_user_sessions()also delete what they find with onebackend.delete_many()call instead of onedeleteper session. (#165) -
The session middleware varies on the header that carried the token.
FastAPICacheXSessionMiddlewareaddedVary: Cookiewheneverrequest.sessionwas accessed, even when the token came inX-Session-TokenorAuthorization, so a shared cache could key those responses on the wrong header. It now varies on every request header it read to find the token, and onCookieonly when no header carried one. (#168) -
The wheel and sdist ship the LICENSE file.
pyproject.tomldeclaredApache-2.0but nolicense-files, so neither artifact contained the licence text the Apache-2.0 licence requires recipients to receive. The package also carries theTyping :: Typedclassifier now. (#232)
rotate_session_id(request)gives the request's session a new ID at login. WithFastAPICacheXSessionMiddleware, a Starlette-style login that only writes torequest.sessionkeeps the session ID the request arrived with, so whoever planted that cookie was logged in too. Call it before attaching the user; with no session loaded it does nothing, since the first write starts a fresh one. The SESSION.md example usedSessionDepand answered401to new visitors; it now uses the helper, and the migration section warns about the difference from Starlette. (#225)- OAuth states can be bound to the browser that started the flow.
create_state(binding=...)stores the SHA-256 of a client secret, such as a nonce also set as a cookie, andconsume_state(state, binding=...)rejects the state withInvalidStateErrorunless the same value is given. Without a binding any stored state completes the flow in any browser, which allowed login CSRF although STATE.md described the states as CSRF protection. The quick start now sets and checks a binding cookie. (#226) request.session.clear()logs out a session whose data was empty. WithFastAPICacheXSessionMiddleware, whether the data started out empty decided what a cleared session meant, so a user session created without data survivedclear()and stayed logged in.clear()on a loaded session now always deletes it; keys written afterclear()go into a new anonymous session. The same rule logged a user out when the last key was removed withdelorpop(), e.g. a flash message; such a session is now saved with empty data. An emptied anonymous session is still deleted. (#227)- A
ttlmust be anintup toMAX_TTL, anddeltaanintin 64-bit range. On Redis,increment(key, ttl=1.5)created the counter and then failed atEXPIRE, leaving a counter that never expired (a permanent lockout for a rate limiter) behind an error saying the key was "not a counter".validate_ttlnow raisesTypeErrorforfloat,booland other types, andValueErroraboveMAX_TTL(2**31 - 1 seconds), before any backend I/O. A float TTL used to work on the memory backend only.incrementchecksdeltathe same way. Memcached now raisesValueErrorfor attlwhose expiry falls after 2038-01-19, which it used to accept and then drop at once, and reports only non-numeric values as "not a counter".@cacherejects such attlwhen the decorator is applied. (#229) - A
|||in theHostheader or path can no longer poison another path's cache entry. The default key builder joined the raw host and decoded path with|||, soGET /xwithHost: example.com|||/pstored its response under the key ofGET /p%7C%7C%7C/x.|and%in the host and path are now percent-encoded (escape_key_componentinfastapi_cachex.types);clear_path()encodes its argument the same way and the monitoring routes decode for display. Keys whose host or path contains|or%change, so those entries are cached afresh once. The HTTP caching guide now recommendsTrustedHostMiddleware. (#230) - Releases run from master only, and only the publish step can reach
PyPI.
release.ymlcould be dispatched on any branch, pushing the release commit there and publishing unmerged code, and the one job that installed every dev dependency also heldcontents: writeandid-token: write. A non-dry-run dispatch offmasternow fails at once. The workflow is split into a read-onlybuildjob, areleasejob that commits, tags and creates the GitHub release, and apublishjob in thepypienvironment that alone can mint the PyPI token. (#231) - The JWT serializer warns about an HMAC key shorter than RFC 7518
requires.
secret_keyneeds only 32 characters, butHS384andHS512need 48 and 64 bytes.JWTTokenSerializernow emits oneUserWarningwhen it is built with a shorter key, instead of relying on PyJWT's per-tokenInsecureKeyLengthWarning. (#116)
- Runnable examples.
examples/holds one complete FastAPI app per feature: HTTP caching,CacheManager, cookie and JWT sessions, OAuth state,CacheLock, a rate limiter onincrement()and a Redis backend.tests/test_examples.pyruns each one's main flow, so they keep working as the library changes. The guide pages link to the matching example. (#241) - Guides checked against 0.3.8. The distributed lock and contributing
guides are now available in Traditional Chinese. The JWT claims guide uses
FastAPICacheXSessionMiddleware, and its custom serializer no longer reads private attributes. Corrected statements:invalidate()raises backend errors, only the Redis and Memcached backends prefix their keys, andCacheLockis a lease rather than a guarantee of mutual exclusion. (#214) - Logging a user in. The session guide now shows how to attach a
SessionUserat login sorequire_user_sessionandAuthenticatedSessionaccept the session; writing torequest.sessionalone does not. (#294)
0.3.7 - 2026-09-25
CacheManager.add(key, value, ttl=None) -> boolstores an application value only when the key is free and reports whether it did. It uses the same key prefix, JSON encoding anddefault_ttlasset(), and runs on the backend's atomicset_if_absent, so of several concurrent callers exactly one wins — for "send this webhook once" style deduplication. (#65)add_routes(..., include_content_preview=False)leaves response bodies out of/cached-records:content_previewisnull, while keys, sizes and expiry are still reported. The default staysTrue. (#79)get_client_ip(request, config)(exported fromfastapi_cachex.session) and theClientIPDepdependency (fastapi_cachex.session.dependencies) return the client address the session middleware checksip_bindingagainst, honouringtrusted_proxies. Pass it tocreate_session(): behind a trusted proxy,request.client.hostis the proxy's address, so a session bound to it was rejected on its next request. (#87)SessionConfig.trusted_proxiesaccepts CIDR ranges (10.0.0.0/8,2001:db8::/32) as well as single addresses, for load balancers that connect from a subnet. It applies to both the peer check and theX-Forwarded-Forwalk. IPv4-mapped IPv6 peers match IPv4 entries, and non-IP entries such astestclientstill match exactly. An entry containing/that is not a valid range now fails config validation. (#73)
-
The Cache-Control table in the HTTP caching guide no longer marks header-only directives as simply "supported". It now shows, for each directive, how to set it, whether it is sent, and what it does to the server-side cache. A new section documents that the request's own
Cache-Controlis ignored by design. -
Docstrings and guides that disagreed with the code are corrected. Most visible:
SessionConfig.sliding_thresholdnow describes renewal once less than that fraction of the TTL remains; it used to say the opposite.- The
cache()argumentsno_cache,stale_ttl,privateandttlare described by what they do, and the docstring gains aRaises:section. - The per-user
key_builderexample in the HTTP caching guide no longer setsprivate=True, which bypassed the backend and made the key builder unused. - The state quick start catches
StateError, so a malformed state is a 400 instead of a 500. - The
get_session_manager500 message and the session dependency docstrings nameFastAPICacheXSessionMiddlewareinstead of the deprecatedSessionMiddleware. - The monitoring routes no longer claim to count cache hits.
-
The Redis backend now matches its key prefix and the path given to
clear_path()literally when it buildsSCANpatterns. Glob characters in them used to be live:clear_path("/files/[draft]")missed the cached entry for that path, and akey_prefixcontaining?or*letclear(),get_all_keys()andclear_pattern()reach keys under other prefixes. Only the pattern passed toclear_pattern()is still a glob. -
StateManagerno longer writes the raw OAuth state to its logs. The state comes from the callback query string, so logging it leaked live tokens and let a caller forge log lines with CR/LF. Log lines now carrystate_ref, the first 12 hex characters of the state's SHA-256. An unknown or expired state inconsume_state()is logged at INFO instead of WARNING, and malformed stored data is logged once at WARNING without a traceback, instead of two ERROR records with a traceback that echoed the stored state. -
Regenerating the session ID of the request's session (the documented defence against session fixation at login) now sends a token for the new ID.
FastAPICacheXSessionMiddlewareused to re-send the loaded token, which named the recordregenerate_session_id()had just deleted, so the user was logged straight back out. Header clients got no token at all. The deprecatedSessionMiddlewarecould overwrite the new token with a sliding-renewed one for the old ID. Both middlewares now notice the changed ID and send the new token through the request's transport.SessionManager.issue_token(session)is the one place tokens are signed. (#103) -
ttlmeans the same thing on every backend. Zero or negative TTLs now raiseValueErrorfromset,set_if_absentandincrementon all built-in backends, from the base-class fallbacks, and fromCacheManagerandStateManager(defaults included). Before, Memcached stored the entry forever, Redis failed withinvalid expire time, and the memory backend expired it at once.Noneremains the way to say "no expiry", andvalidate_ttl()infastapi_cachex.backends.baselets third-party backends apply the same rule.@cache(ttl=0)stays valid: it sendsmax-age=0and keeps the entry only for ETag revalidation, likettl=None, instead of answering 500 on Redis or replaying the first response forever on Memcached. A negative@cachettl raisesCacheXErrorat decoration time. (#102) -
A
@cachehandler that returns plain data instead of aResponseis rendered the way FastAPI renders it. The result goes through the route's response model (validation, field filtering and theresponse_model_*options) orjsonable_encoder, so a Pydantic model,datetimeorUUIDno longer fails to encode. The route'sstatus_codeapplies (a204drops the body), and the status and headers set on an injectedresponse: Responseare kept, on cache hits as well. (#99) -
A sync (
def) handler under@cacheruns in the threadpool again. The cache wrapper isasync, so FastAPI stopped offloading the handler and@cachecalled it on the event loop, where blocking I/O stalled every other request. A handler whose call returns an awaitable (an object with anasync def __call__, or a sync callable returning a coroutine) is now awaited instead of failing to encode. (#100) -
CacheManager.get_or_set()awaits whatever the factory returns when it is awaitable. The documentedget_or_set(key, lambda: load_user(42))form used to store the coroutine itself and fail withTypeError. (#101) -
The monitoring routes from
add_routes()now show when Redis entries expire.AsyncRedisCacheBackend.get_cache_data()reported every entry as never expiring (ttl_remaining: null); it now fetches each key'sPTTLin the same pipeline as its value and returns the absolute expiry the memory backend reports. A key that disappears between the scan and the fetch is left out. (#74) -
The client IP used for
ip_bindingnow walks everyX-Forwarded-Forheader line, not only the first. A proxy that adds its own line instead of appending to the caller's left a caller-chosen first line in charge of the walk, so a forged address could satisfy the binding. (#104)
- The guides are available in Traditional Chinese at
https://fastapi-cachex.readthedocs.io/zh-tw/latest/, with a language
switcher on both sites. English stays the source of truth: every translated
page says it may lag behind and links to its English original. The API
reference and the development guides remain English-only.
docs/README.zh-TW.mdis replaced by the translated home page. (#89)
0.3.6 - 2026-09-25
BaseCacheBackend.set_if_absent(key, value, ttl=None) -> boolanddelete_if_equals(key, expected) -> bool, the atomic pair for locks and per-user slots: claim a key only when it is free, and release it only while it still holds your entry, so a holder whose entry expired cannot free a slot someone else has claimed since. Redis usesSET NX EXand a Lua compare-and-delete, MemcachedADDandGETS+CAS, memory its lock. Third-party backends inherit non-atomic fallbacks. (#62)
- Concurrent first requests to the
AppCachedependency, in an app that never configured a backend, no longer each build their ownMemoryBackendandCacheManager.get_app_cacheruns in worker threads, so the last one registered replaced the others and, for a while, requests used caches that could not see each other's entries. The lazy set-up and the@cachefallback now happen under one lock. (#76) - A JWT session configured with an asymmetric
jwt_algorithm(RS*,ES*,PS*,EdDSA) and the built-in serializer now fails when theSessionManageris built, with aValueErrorthat names the fix. The built-in serializer only has thesecret_keystring, so such a configuration was accepted at startup and then failed inside PyJWT on the firstcreate_session(). Only the HMAC algorithms work without a customtoken_serializer; configurations that pass one are unaffected. (#86)
- The documentation is published at https://fastapi-cachex.readthedocs.io/,
built with Zensical and including an API reference generated from the
docstrings. The package metadata links to it as
Documentation. - Every guide is now in English and was checked against the code. Among the
corrections:
docs/JWT_CLAIMS.mdtold you to install a custom token serializer by assigningmanager._token_serializer, which has no effect — passtoken_serializer=toSessionManagerinstead; and the cache-flow guide saidclear()is a no-op on Memcached, when it runsflush_alland empties the whole server. README.mdis now a short landing page. Its reference material moved to new guides: HTTP caching, Application cache and Backends.StateManager.create_state()no longer documentsStateDataErrorfor backend failures: backend errors propagate unchanged. (#88)
- The changelog test takes the previous version from the latest released heading instead of naming it, so it no longer has to be edited after every release.
0.3.5 - 2026-09-15
SessionMiddlewareno longer trustsX-Forwarded-For/X-Real-IPby default. Forwarded addresses are only honoured when the peer is listed in the newSessionConfig.trusted_proxies, and the address taken is the rightmost entry that is not a trusted proxy — the leftmost entry is attacker-controlled. If you run behind a reverse proxy and rely onbind_ip, you must now settrusted_proxies, otherwise IP binding sees the proxy's address.trusted_proxiesmatches exact strings; CIDR ranges are not supported yet.- JWT session tokens reject unsafe algorithms (
noneand asymmetric algorithms fed a symmetric key) instead of accepting them. private=Trueresponses are no longer written to or read from the shared backend. Previously a response marked private was still stored where every other caller could read it; onlyIf-None-Matchrevalidation is kept.- Check your key builder if you copied the per-user caching example from the
README of 0.3.4 or earlier. That example built the cache key from an
unverified
X-User-Idrequest header, immediately above a paragraph warning against exactly that. Code copied from it is a horizontal privilege escalation: sendingX-User-Id: <someone-else>returns that user's cached response. The example now reads an identity the authentication layer verified and wrote torequest.state, and the warning is a CAUTION block showing the header version as an explicit anti-example.
fastapi_cachex.__version__, read from the installed distribution metadata.docs/STATE.md: documentation for the previously undocumented state subsystem (StateManager, one-shotconsume_state(), the dependency injection helpers).
SessionMiddlewareis now scheduled for removal in 0.4.0 rather than 0.3.5. Nothing about the class changes — it has emitted aDeprecationWarningsince 0.3.1 and still does — but 0.3.5 is a patch release, and removing an exported public class in a patch release is a breaking change no matter how small the migration is. The runtime warning, the docstring and the guides all name 0.4.0 now, which is wheredelete()returningbooland the removal ofBackendProxy.get_backend()/set_backend()were already scheduled.
- The
starletteextra. All it ever pulled in wasitsdangerous, which is a base dependency now, so the extra adds nothing. Installingfastapi-cachex[starlette]still resolves — an unknown extra is a warning, not an error — it simply has no effect.
itsdangerousis a required dependency rather than an extra, soFastAPICacheXSessionMiddlewareworks on a plainpip install fastapi-cachex. It reusesstarlette.middleware.sessions.Sessionfor its dict-likescope["session"], and that module importsitsdangerousat module level, so the middleware could never be constructed without it — the extra only moved the failure from install time to runtime.- A cache hit now replays the status code and the headers the handler produced,
instead of always returning
200with no headers. Entries written by earlier versions are still readable and replay as200. If-None-Matchfollows RFC 9110 §8.8.3.2: weak comparison,*, and multi-value headers all match correctly. A304now carries theCache-Control,Content-Location,ExpiresandVaryheaders the200would have carried (§15.4.5), rather than onlyETagandCache-Control.clear_pattern()globs the whole cache key on every backend. Previously each backend interpreted the pattern differently; a pattern shaped like a bare path now clears nothing and emits aRuntimeWarningsaying so, instead of silently matching nothing.- Memcached keys longer than the protocol's 250-byte limit, or containing bytes it refuses, are stored under a SHA-256 digest instead of failing. TTLs beyond 30 days are sent as an absolute timestamp, as the protocol requires — they previously expired immediately.
- The
MemoryBackendsweeper starts on writes as well as reads, so a write-only workload (for exampleStateManager.create_state) no longer accumulates expired entries. @cachehandlers that take**kwargs, or that annotateRequestas a string, no longer crash when the decorator injects the request parameter.
AppCachefalls back to aMemoryBackendwhen no backend is configured, matching what@cachealready did.
docs/CACHE_FLOW.mdcorrected against the code: the decorator parameter isttl(notmax_age), the cache key separator is|||(not:), network backends store latin-1 round-tripped JSON (not base64), and the entry structure matches the currentCacheEntry/CacheItemdataclasses.README.mddocumentsinvalidate(),add_routes(),CacheManager.get_or_set()/clear_pattern(),RedisConfig.load_from_config()and the extras. It now warns that the monitoring endpoints have no authentication and that/cached-recordspreviews cached content, and notes that the Redis backend reports every entry as never expiring becauseget_cache_data()returns no TTL.docs/SESSION.mdstates thatSessionMiddlewareis deprecated since 0.3.1 and will be removed in 0.4.0, and that cookie transport is provided only byFastAPICacheXSessionMiddleware.
- The Redis and Memcached suites are now opt-in: they wipe the server they
connect to, so they skip unless
CACHEX_TEST_REDIS_PORT/CACHEX_TEST_MEMCACHED_PORTnames one. Contributors must point them at a throwaway server; seedocs/DEVELOPMENT.md. CACHEX_REQUIRE_LIVE_SERVERS=1, set by every CI workflow, makes a skipped live-server suite fail the run. Opting in kept a straypytestfrom wiping a developer's data, but it also meant a mistyped port silently dropped those suites while coverage stayed near 97% and the job went green.
- Releases are cut by one workflow instead of two.
publish.yml(patch) andrelease.yml(minor) were the same steps twice over, so which part of the version a release moved depended on which Actions page was opened;Releasenow takes the bump as an input, with an exact version as an override. - The GitHub release notes are this file's
## [Unreleased]section, promoted byscripts/changelog_release.py, rather than a list of commit subjects. A release with an empty## [Unreleased]stops instead of publishing notes that say nothing; the commit list is still reachable through the compare link at the end of the notes. - The release runs the full test suite — including the Redis and Memcached suites, which cannot skip there — before it writes, tags or publishes anything, and refuses a version that is already tagged.
- The release can be rehearsed: dispatching it with
dry_runruns the gate, the version bump, the changelog promotion and the build, then stops short of the four steps that commit, tag, release and publish. The release notes and the built distributions are attached to the run so they can be inspected before the real thing.
0.3.4 - 2026-09-05
- Atomic backend primitives on
BaseCacheBackend, overridden by every built-in backend:increment()(Redis Lua script, MemcachedADD+INCR) andget_and_delete()(RedisGETDEL, Memcached get + acknowledged delete).StateManager.consume_state(),delete_state(),CacheManager.delete()andinvalidate()use them, making one-shot retrieval genuinely atomic. delete_many(), batched into a singleDELon Redis and a single lock acquisition on Memory;clear_prefix()is built on it.
- The Memcached client pools connections and waits for write acknowledgements, so concurrent calls no longer share a socket and a write is visible to the next read.
0.3.2 - 2026-07-29
- Cached responses use the application's configured default response class instead of a hard-coded one.
0.3.1 - 2026-07-07
FastAPICacheXSessionMiddleware: backend-backed session middleware that supports cookie transport as well as theX-Session-Tokenheader andAuthorization: Bearer, and routes the response side by token source.CacheManager.get_or_set()andCacheManager.clear_pattern().invalidate()for busting a single cached route.StateManagerProxyand theget_state_managerdependency.
SessionMiddleware, in favour ofFastAPICacheXSessionMiddleware. It emits aDeprecationWarningat construction and will be removed in 0.3.5.
MemoryBackend.clear_pattern()no longer misses keys without a separator.SessionConfigrejects unknown fields instead of silently ignoring them.
0.3.0 - 2026-07-02
Baseline for this changelog. Earlier releases are described in the GitHub releases.