cachier: a persistent cache decorator for Python functions
Persistent, stale-free, local and cross-machine caching for Python functions.
At a glance
- What is it?
- cachier turns any Python function into one with a persistent, optionally expiring cache, backed by pickle files, memory, MongoDB, SQL, Redis or S3. It is built for calls that take more than about a second, and it protects you from a class of bug where a method cache silently mixes state from different object instances.
- Who is it for?
- Adopt cachier when a function call is slow enough to be worth a disk or database round trip and you want the result to survive between Python processes, and pick the backend that matches where you need the cache to live. Do not adopt it as a transient in-process cache, because the documentation points at @lru_cache for that, and do not ignore the instance method restriction without reading what it protects you from.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The decorator, and what it will not do for you
cachier is a decorator. You wrap a function with it and the function keeps a persistent cache keyed by its arguments, so a slow calculation runs once and every later call with the same arguments returns the stored result.
The documented installation is a single pip command:
pip install cachierPython 2.7 support ended with version 1.2.8, and that older release is still installable for anyone stuck:
pip install 'cachier==1.2.8'The project states its own limits as clearly as its features, which is unusual and welcome. It is not meant as a transient cache, and it says so in terms: Python's @lru_cache is better for that. It is also not especially fast, and the reason given is that it is meant to replace function calls that take more than about a second, with overhead around 1 millisecond.
That framing decides the use case. If your function returns in microseconds, the disk or network round trip is pure loss. If it takes seconds and gets called with the same inputs repeatedly, across scripts or across processes, cachier turns it into a fast lookup.
Two other constraints come with the decorator. The positional and keyword arguments to the wrapped function must be hashable, so immutable built-in objects rather than mutable containers. And user-defined class instances are hashable but compare unequal, since their hash is their id, which means equal objects across different sessions do not produce identical cache keys.
Six cache backends behind one decorator
The interesting part of cachier is that the storage location is a choice, and the list of cores covers the range from a single file to a shared cluster.
Local caching uses pickle files. In-memory caching uses the memory core. Cross-machine caching uses MongoDB. SQL-based caching uses SQLAlchemy-supported databases. Redis-based caching is there for high-performance scenarios, and S3-based caching covers cross-machine object storage backends.
The dependency list in pyproject.toml shows how these are packaged. The required dependencies are only three, portalocker for file locking, pympler and watchdog, so the default local backend has a small footprint. Everything else is optional: boto3 for S3, pymongo for MongoDB, redis for Redis, and sqlalchemy for the SQL backend, each available as its own extra and also bundled in an all extra.
That structure matters for a library that is imported into applications you do not control. You install cachier with no extras and get the pickle backend with no database driver in your dependency tree. You add the extra for the backend you actually use.
The distinction between the memory core and the pickle core is the one to keep straight. The memory core keeps values in the running process, so it does not survive a restart and does not help across kernels. The pickle core writes files, which is what the documentation means by a cache that lasts across different Python kernels calling the wrapped function.
The S3 core is the newest addition in the release history. Version 4.2.0 is titled S3 cores plus async support, and it arrived on 2026-03-26.
The instance method guard, and how to opt into it
This is the sharpest design decision in the library, and it protects against a bug that is easy to write and hard to spot.
Cachier ignores the self parameter when computing the cache key for an object method. That means methods are cached across all instances of an object. The documentation is blunt about the consequence: this means that methods will be cached across all instances of an object, which may not be what you want.
Rather than leave that as a footnote, the decorator raises a TypeError by default when applied to an instance method, defined as a function whose first parameter is named self. The error is raised when @cachier is applied, at class definition time, not when the method is called. Catching it at import time rather than at call time is the useful part, because a wrong cache hit is invisible until it corrupts a result.
The opt-in is explicit. Passing allow_non_static_methods=True tells cachier you have thought about it:
from cachier import cachier
class Foo():
@staticmethod
@cachier()
def good_static_usage(arg_1, arg_2):
return arg_1 + arg_2
# Instance method does not depend on object's internal state, so good to cache
@cachier(allow_non_static_methods=True)
def good_usage_1(self, arg_1, arg_2):
return arg_1 + arg_2The example then shows the case the guard exists for. A method that reads an instance attribute and is cached with plain @cachier() raises the TypeError, and the comment marks that as intentional rather than as a limitation to work around.
Note also the decorator ordering in the static method example, where @staticmethod comes before @cachier(). That ordering is what makes the first argument a real argument rather than self.
Shelf life, stale values and cleanup
The defining feature is that a cached value can have a lifetime, and once that lifetime passes the function runs again.
You express it with a timedelta passed to stale_after:
import datetime
@cachier(stale_after=datetime.timedelta(weeks=2))
def bar(arg1, arg2):
return {'arg1': arg1, 'arg2': arg2}When a cached value matching the given arguments is found, the time of its calculation is checked. If more than the stale_after interval has passed, the function runs again for the same arguments and the new value is cached and returned.
The documentation names the use case for this precisely: lengthy calculations that depend on a dynamic data source. The alternative, invalidating the cache by hand every time the underlying data changes, requires you to know when the data changed. Expiring on a time interval does not, and it is a good fit when the input data changes on a schedule you can bound rather than on an event you can observe.
Two related mechanisms extend this. There is a per-call max age, so you can specify a maximum age for cached values per call rather than only at decoration time. And version 4.1.0 added automatic cleanup of stale cache values, released on 2025-07-15, which is what you want once entries start expiring on their own: without cleanup, a pickle core keeps growing with entries nobody will read again.
There is also a fuzzy shelf life concept described in the documentation for the case where you want the function to trigger on a less exact basis, and the release history shows the storage directory convention also changed, with version 4.0.0 adding support for an XDG-compliant cache directory.
Global defaults, the parameter split and the worker limit
Settings can be applied across every cachier wrapper through set_default_params, which takes the same keyword parameters as the decorator. Parameters given directly to a decorator take precedence.
The defaults are not all equivalent, and the documentation splits them by when they take effect. Some parameters only apply to decorators defined after set_default_params is called, which is the case for hash_func, backend, mongetter, cache_dir, pickle_reload, separate_files, entry_size_limit and allow_non_static_methods. The reason is structural: these choose where and how the cache is stored, so a wrapper created earlier already points at a specific location.
The rest can be changed at any time and apply to all decorators, including allow_none, caching_enabled, stale_after, next_time, wait_for_calc_timeout, cleanup_stale and cleanup_interval. These affect behaviour rather than identity, so flipping them affects wrappers that already exist. The current values can be read back with get_default_params.
That split has a practical consequence. If you call set_default_params with a backend after some modules have already been imported and their functions decorated, those earlier decorators keep the old backend, silently. Setting defaults early in the process, before your application modules are imported, avoids the surprise.
Two global controls are documented separately. Caching can be turned off across all decorators with disable_caching and reactivated with enable_caching, which are described as convenience wrappers around the caching_enabled default setting. And the number of threads cachier is allowed to spawn is limited by the CACHIER_MAX_WORKERS environment variable, with a default of 8.
Resetting is per function. The wrapper adds a clear_cache() function to each wrapped function, so you call foo.clear_cache() to reset that one cache.
The tooling around the library and what it does not promise
The repository is a modern Python project and the surrounding tooling tells you how much attention the library gets.
pyproject.toml uses setuptools and wheel for the build, with the package living under src, and it declares a console script named cachier pointing at cachier.__main__:cli. The extras for mongo, redis, s3 and sql exist as separate entries, and the uv.lock file at the root shows that the project is managed with uv as well as being pip installable.
The Makefile is written for running the test suite against every backend. There are targets for all tests, local tests, external backend tests for mongo, redis and sql, and specific targets per backend such as test-mongo-local, test-redis-local and test-sql-local, several of which run under Docker. There are service targets to start, stop and tail the containers, and separate MongoDB targets. Code quality is handled by three targets, make lint for ruff, make type-check for mypy and make format for ruff, and there are install, install-dev and install-all targets.
The examples directory holds five runnable scripts: async_example.py, metrics_example.py, prometheus_exporter_example.py, redis_example.py and s3_example.py. The metrics and Prometheus exporter examples line up with the documented cache analytics and observability feature, which tracks cache performance metrics including hit rates and latencies.
What the project does not promise is worth stating. It is not especially fast, it is not a transient cache, and it explicitly tells you to use @lru_cache for that. The classification in pyproject.toml is Development Status :: 4 - Beta, which is an honest signal about where it sits. Python support is 3.10 and later, with classifiers for 3.10 through 3.14, and the licence is MIT.
Editorial conclusion
Adopt cachier when a function call is slow enough to be worth a disk or database round trip and you want the result to survive between Python processes, and pick the backend that matches where you need the cache to live. Do not adopt it as a transient in-process cache, because the documentation points at @lru_cache for that, and do not ignore the instance method restriction without reading what it protects you from. Before enabling it on a class, decide whether your method depends on instance state, because cachier raises a TypeError at class definition time unless you pass allow_non_static_methods=True.
Frequently asked questions
How do I install cachier and add a cache to a function?
Install it with pip install cachier, then decorate the function with @cachier(). The arguments to the wrapped function must be hashable, and the default pickle backend makes the cache persist across different Python kernels.
Why does cachier raise a TypeError on my instance methods?
By default @cachier ignores the self parameter when building the cache key, which would share cached values across all instances of an object. Rather than allow that silently, cachier raises a TypeError at class definition time for methods whose first parameter is named self, and you opt in with allow_non_static_methods=True.
Which cache backends does cachier support?
Local caching with pickle files, in-memory caching with the memory core, MongoDB for cross-machine caching, SQL through SQLAlchemy-supported databases, Redis for high performance, and S3 for cross-machine object storage. The extra packages are optional dependencies in pyproject.toml.
How do I set a shelf life for cached values in cachier?
Pass a timedelta to the stale_after parameter, for example @cachier(stale_after=datetime.timedelta(weeks=2)). When a stored value is older than that interval, the function runs again and the new value is cached.
Is cachier a substitute for lru_cache?
No. The documentation states that cachier is not meant as a transient cache and that Python's @lru_cache is better for that, and it notes that cachier is not especially fast, with overhead around 1 millisecond, so it targets calls taking more than about a second.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/python-cachier-cachier)Was this useful?