requests-cache: a persistent HTTP cache for Python requests
Persistent HTTP cache for python requests
At a glance
- What is it?
- requests-cache is a drop-in cache for the requests library, giving you a session that stores responses on disk, honours Cache-Control and other expiration headers, and serves later requests from SQLite, Redis, MongoDB, DynamoDB or plain files. It suits a scraper or a test suite that hits the same endpoints repeatedly, and its matching rules are configurable enough to be trusted with parameterised URLs.
- Who is it for?
- Adopt requests-cache if your Python code requests the same URLs repeatedly, whether in a test suite, a backtesting run or a scraper, because it turns sixty one second calls into one call and sixty instant ones without changing the shape of your code. Do not adopt it for an API client whose responses must be fresh, and do not enable the global patching mode in a library without thinking about it, since it changes the behaviour of requests for every importer.
- Can I use it commercially?
- Yes. BSD-2-Clause 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 6 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 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it does and where it sits
requests-cache is a persistent HTTP cache that provides an easy way to get better performance with the requests library, and the summary in the README is unembellished about the goal.
The design decision that matters is that it works at the level of the requests session, so the code you are caching keeps looking the way it always did. The library replaces requests.Session with a CachedSession, and every other part of the code stays as it is.
The headline performance claim is specific. You get sub-millisecond response times for cached responses, and when the cached responses expire you still save time, because the library falls back to conditional requests. A conditional request is the standard HTTP mechanism for asking a server whether the copy you have is still current, so an expired entry costs a request that returns almost nothing rather than a full download.
That second half is the part that separates this from a naive cache, and it is why the library is usable against live sites rather than only against fixtures.
The persistence is the other half of the value. A cache in memory disappears when the process does, so a test suite that runs in one process and a scraper that runs in batches both need something on disk, and that is what the backends provide.
The package is BSD-2-Clause licensed, and the manifest classifies it as Production/Stable, with the requirement of Python 3.8 or later and classifiers running up to 3.15. The last push to the repository was on 2026-10-17, and the most recent tagged release is 1.3.0 from February 2026.
Two ways in: a session, or a global patch
The library offers two integration modes, and the second one is the one to think about before you use it.
The first is the session. You install the package and use CachedSession in place of a normal session.
pip install requests-cacheimport requests_cache
session = requests_cache.CachedSession('demo_cache')
for i in range(60):
session.get('https://httpbin.org/delay/1')The contrast in the README is between a plain requests session that takes a minute for sixty calls to an endpoint that delays by a second, and the cached session that takes a second in total, because the response is fetched once, saved to a SQLite file and served from there afterwards.
The second mode is global patching, and the README describes it as the way to quickly test it out in your application without modifying any code.
import requests
import requests_cache
requests_cache.install_cache('demo_cache')
requests.get('https://httpbin.org/delay/1')That installs the cache so that all requests functions are transparently cached, and it is the mode that has consequences. Patching a library that other code imports changes their behaviour too, and in a project where requests is used by a dependency rather than by you, that is not a decision you should make by accident. The library is explicit that this is for trying it out, and the session mode is the one to use in code you own.
Expiration, in three forms
By default requests-cache keeps cached responses indefinitely, and the README says that in most cases you will want one of two strategies to balance cache freshness and performance.
The first is to define exactly how long to keep responses, with the expire_after parameter, either as a number of seconds or as a timedelta.
from requests_cache import CachedSession
from datetime import timedelta
# Keep responses for 360 seconds
session = CachedSession('demo_cache', expire_after=360)
# Or use timedelta objects to specify other units of time
session = CachedSession('demo_cache', expire_after=timedelta(hours=1))The second is to use the server's own opinion, with the cache_control parameter enabling automatic expiration based on Cache-Control and other standard HTTP headers sent by the server.
from requests_cache import CachedSession
session = CachedSession('demo_cache', cache_control=True)The third, mentioned in the feature list, is the backends that support TTL natively, which lets the storage layer expire entries for you without the library having to sweep them.
The distinction between the first two is worth pausing on, because it decides how much you trust the cache. A fixed schedule is your policy, applied to everything. Cache-Control is the server's policy, applied per URL, and it is the one that gets the details right, because the server knows whether a particular endpoint is a live feed or a static asset. Using both together is a normal arrangement, and the settings example shows exactly that.
The settings that decide what gets cached
The settings example is worth reading closely, because these are the options that determine whether the cache is correct rather than merely fast.
session = CachedSession(
'demo_cache',
use_cache_dir=True, # Save files in the default user cache dir
cache_control=True, # Use Cache-Control response headers for expiration, if available
expire_after=timedelta(days=1), # Otherwise expire responses after one day
allowable_codes=[200, 400], # Cache 400 responses as a solemn reminder of your failures
allowable_methods=['GET', 'POST'], # Cache whatever HTTP methods you want
ignored_parameters=['api_key'], # Don't match this request param, and redact it from the cache
match_headers=['Accept-Language'], # Cache a different response per language
stale_if_error=True, # In case of request errors, use stale cache data if possible
)The two that matter most for correctness are ignored_parameters and match_headers. The first says that a given query parameter should not be part of the cache key, and it also redacts that parameter from the cached copy, which stops an API key ending up in a file on disk. The second says that a particular header should be part of the key, so that a request in one language does not get a response in another.
The default behaviour is to key on the method and the URL, and that is right for the simple GET case. As soon as a parameter or a header changes the meaning of the response, it has to be in the key, and the two options are how you say so.
stale_if_error is the other one to notice, since it means that when the request fails, the library can serve the stale cached copy rather than propagating the error. That is a resilience feature with a correctness cost, since the caller receives data that is not current, so it is a decision rather than a default.
The backends, and the compose file that proves them
The persistence options are the reason this is more than a wrapper, and they are listed in the features.
The storage backends include SQLite, Redis, MongoDB and DynamoDB, and there is also the option of saving responses as plain JSON files, YAML and more. The file based backends matter more than they sound, because a cache that is a directory of readable files can be inspected, diffed and shipped around, while a cache in Redis is a blob.
The compose file in the repository is the clearest evidence of how broad the backend support is, because it starts the services needed to test all of them locally. There is a go-httpbin container on port 8080 to serve as the test endpoint, a DynamoDB local container on port 8000 running in memory with placeholder credentials, a MongoDB container on 27017 with a database named for the test suite, a Redis container on 6379, and a Valkey container on 6380.
That list of five services for a cache library is a statement about the project's ambition, and it explains the release history. Version 1.3 added an LRU filesystem backend, alongside performance and memory use improvements and compatibility fixes, and 1.1 was about bugfixes and SQLite concurrency improvements, which is the kind of fix that matters when several processes share one cache file.
The dependency list supports the same picture. Beyond requests and urllib3, there are attrs and cattrs for the response data models and their serialisation, platformdirs for the system directory handling that the use_cache_dir option needs, and url-normalize, which the manifest explains is there for more accurate request matching. That last one is a quiet but important choice, since two URLs that differ only in the order of their query parameters or in their default port should be the same request.
The alternative, and the state of the project
There are two other ways to cache HTTP responses from Python, and the difference is where the cache lives.
The first is a caching proxy in front of your code, which is transparent to the application and needs no changes at all. The trade is that it is a separate process to run and secure, and that it sits outside your program so it cannot be keyed on the things your program knows, such as which user asked or which locale it is rendering in.
The second is to write it yourself with a requests adapter, which is in-process and can key on anything, and which means implementing the cache key, the expiry rules and the storage yourself. That is a day of work for something that has been maintained for years.
requests-cache sits in the middle. It is in-process, so it can key on request details, and it is a library, so the hard parts are already written. What it does not do is cache anything the requests library does not see, and it does not cache across different processes unless they share a backend, which is where Redis, MongoDB and DynamoDB come in.
On the state of the project, the manifest version is 1.3.4 while the latest tagged release is 1.3.0 from February 2026, so the main branch is carrying work that has not been tagged, which is the normal arrangement for a project that releases from a release branch. The push to the repository on 2026-10-17 shows the work is current.
The examples directory is the best tour of the features, with files for patching, sessions, benchmarks, converting a cache, a custom request matcher, expiration, external configuration, generating a test database, logging requests, threads, time machine backtesting, URL patterns and a VCR example, plus a script that plots requests per second.
Editorial conclusion
Adopt requests-cache if your Python code requests the same URLs repeatedly, whether in a test suite, a backtesting run or a scraper, because it turns sixty one second calls into one call and sixty instant ones without changing the shape of your code. Do not adopt it for an API client whose responses must be fresh, and do not enable the global patching mode in a library without thinking about it, since it changes the behaviour of requests for every importer. Before you point it at a real API, check the matching settings, particularly ignored parameters and match headers, because a cache that matches too widely will serve one response to a request that should not have had it.
Frequently asked questions
What is requests-cache?
It is a persistent HTTP cache for the requests library, providing Django style access with a drop-in replacement for requests.Session or a global install that caches all requests transparently. It supports several storage backends including SQLite, Redis, MongoDB and DynamoDB, as well as plain JSON and YAML files.
How do I make requests-cache expire responses?
With the expire_after parameter for a fixed schedule, given as seconds or as a timedelta, with the cache_control parameter to follow Cache-Control and other standard HTTP headers from the server, or with a backend that supports TTL natively. The settings example combines the first two.
How do I stop the cache from serving the wrong response?
Use the matching options. The ignored_parameters list names query parameters that should not be part of the cache key and are also redacted from the cached copy, and match_headers names headers such as Accept-Language that should produce separate cache entries. The default key is the method and the URL.
Is there a way to cache responses when a request fails?
Yes, the stale_if_error setting makes the library use stale cached data when a request error occurs. It is a resilience measure with a correctness cost, since the caller then receives data that is not current, so it is worth enabling deliberately.
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/requests-cache-requests-cache)Was this useful?