Welcome to Falcon-Caching’s documentation!

Version: 1.1.0

Falcon-Caching adds cache support to the Falcon web framework.

It is a port of the popular Flask-Caching library to Falcon.

The library aims to be compatible with CPython 3.7+ and PyPy 3.5+.

You can use this library both with a sync (WSGI) or an async (ASGI) app, by using the matching cache object (Cache or AsyncCache). Throughout the documentation we will be mostly be showcasing examples for the Cache object, but all those example could be used with the AsyncCache object too. The Quickstart example shows both Cache and AsyncCache side-by-side. Obviously you should never be mixing the two in a single app, use one or the other.

Quickstart

WSGI (alias sync) example:

import falcon
from falcon_caching import Cache

# setup the cache instance
cache = Cache(config={'CACHE_TYPE': 'simple'})

class ThingsResource:

    # mark the method as cached
    @cache.cached(timeout=600)
    def on_get(self, req, resp):
        pass

# create the app with the cache middleware
# you can use falcon.API() instead of falcon.App() below Falcon 3.0.0
app = falcon.App(middleware=cache.middleware)

things = ThingsResource()

app.add_route('/things', things)

ASGI (alias async) example:

import falcon.asgi
from falcon_caching import AsyncCache

# setup the cache instance
cache = AsyncCache(config={'CACHE_TYPE': 'simple'})

class ThingsResource:

    # mark the method as cached
    @cache.cached(timeout=600)
    async def on_get(self, req, resp):
        pass

app = falcon.asgi.App(middleware=cache.middleware)

things = ThingsResource()

app.add_route('/things', things)

Alternatively you could cache the whole resource (watch out for issues mentioned in Resource level caching):

# mark the whole resource as cached
@cache.cached(timeout=600)
class ThingsResource:

    def on_get(self, req, resp):
        pass

    def on_post(self, req, resp):
        pass

Warning

Be careful with the order of middlewares. The cache.middleware will short-circuit any further processing if a cached version of that resource is found. It will skip any remaining process_request and process_resource methods, as well as the responder method that the request would have been routed to. However, any process_response middleware methods will still be called.

This is why it is suggested that you add the cache.middleware following any authentication / authorization middlewares to avoid unauthorized access of records served directly from the cache.

Installation

Install the extension with pip:

$ pip install Falcon-Caching

Set Up

Cache is managed through a Cache and the AsyncCache instance:

import falcon
# import falcon.asgi
from falcon_caching import Cache, AsyncCache

# setup the cache instance
cache = Cache(  # could also be 'AsyncCache'
    config=
    {
        'CACHE_EVICTION_STRATEGY': 'time-based',  # how records are
                                                  # evicted
        'CACHE_TYPE': 'simple'  # backend used to store the cache
    })

class ThingsResource:
    # mark the method as cached for 600 seconds
    @cache.cached(timeout=600)
    def on_get(self, req, resp):   # this could also be an async function
        pass                       # if AsyncCache() is used

# create the app with the cache middleware
# you can use falcon.API() instead of falcon.App() below Falcon 3.0.0
app = falcon.App(middleware=cache.middleware)
# app = falcon.asgi.App(middleware=cache.middleware)


things = ThingsResource()

app.add_route('/things', things)

Eviction strategies

Once a resource is cached, there is the question of how that cached record will be evicted from the cache - alias what ‘eviction strategy’ is followed.

Below is the list of supported strategies:

‘time-based’

The most well known eviction strategy is simply time-based, meaning that the cached record gets evicted based on a timeout (also called TTL, time-to-live) being reached. In this case the cached data is invalidated x seconds after it was generated. In our library this is called ‘time-based’ eviction and it is the default eviction strategy.

‘rest-based’

For REST APIs - which implement the RESTful methods closely - there is another possible option, to evict records based on the definition of the RESTful methods.

In this case GET requests are the only ones cached, but those are cached indefinitely. They only get removed from the cache when another request of the same resource of type PUT / PATCH / POST or DELETE arrives. This will invalidate/evict the cached record and force the next GET request to re-cache it. We call this ‘rest-based’ eviction strategy.

‘rest-and-time-based’

The third option is a combination of these two, where the eviction happens based on whichever of these two events occurs first - the time expires or a PUT/PATCH/POST/DELETE request arrives. We call this ‘rest-and-time-based’ eviction strategy.

These eviction strategies can be set with the CACHE_EVICTION_STRATEGY config attribute - see Configuring Falcon-Caching.

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'simple',
        'CACHE_EVICTION_STRATEGY': 'rest-based'
    })

If no CACHE_EVICTION_STRATEGY is provided then the ‘time-based’ strategy is used by default.

Backends (alias ‘CACHE_TYPE’)

When you are caching you have the choice of what kind of backend to cache to, be that a Redis database, Memcached, the local process’ memory or just files on the local filesystem.

The Falcon-Caching library offers you different backend options and made to be extendable, so additional backend options can be added.

The type of backend used is determined by the CACHE_TYPE attribute - see Configuring Falcon-Caching.

Below is an example of using CACHE_TYPE with value ‘simple’ - which makes the cached records stored in the local process’ memory (not 100% thread safe!):

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'simple',  # backend 'simple' will be used
        'CACHE_EVICTION_STRATEGY': 'time-based'
    })

Note

Credits must be given to the authors and maintainers of the Flask-Caching library, as the structure and much of the code of our backends was ported from their popular library.

Below is a list of available backends, alias the available CACHE_TYPE options:

‘simple’ (the default)

A simple memory cache for single process environments. This option exists mainly for the development server and is not 100% thread safe. It tries to use as many atomic operations as possible and no locks for simplicity, but it could happen under heavy load that keys are added multiple times. Do not use in production!

Example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'simple',  # backend 'simple' will be used
        'CACHE_EVICTION_STRATEGY': 'time-based'
    })

‘null’

A cache that doesn’t cache. This can be useful for unit testing.

‘filesystem’

A cache that stores the items on the file system. This cache depends on being the only user of the ‘cache_dir’. Make absolutely sure that nobody but this cache stores files there or otherwise the cache will randomly delete files therein.

Example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'filesystem',
        'CACHE_EVICTION_STRATEGY': 'time-based',
        'CACHE_DIR': '/tmp/falcon-cache-dedicated/',
        'CACHE_THRESHOLD': 500  # the maximum number of items the
                                # cache stores before it starts
                                # deleting some. A threshold value
                                # of 0 indicates no threshold.
                                # default: 500
    })

‘redis’

A cache that stores the items in the Redis key-value store or an object which is API compatible with the official Python Redis client (redis-py).

If you want to use an object which is API compatible with the official Python Redis client (redis-py), then just supply that as an initialized object to the CACHE_REDIS_HOST parameter.

If you use the same Redis database for other purposes too, then you are strongly advised to specify the CACHE_KEY_PREFIX, so keys would not accidentally collide and cache.clean() calls would only remove keys from the cache and not other records.

Example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'redis',
        'CACHE_EVICTION_STRATEGY': 'time-based',
        'CACHE_REDIS_HOST': 'localhost',  # Redis host/client object
                                          # default: 'localhost'
        'CACHE_REDIS_PORT': 6379,  # default: 6379
        'CACHE_REDIS_PASSWORD': 'MyRedisPassword',  # default: None
        'CACHE_REDIS_DB': 0,  # default: 0
        'CACHE_KEY_PREFIX': 'mycache'  # default: None
    })

Alternatively you could also supply a Redis URL via the CACHE_REDIS_URL argument, like redis://user:password@localhost:6379/2.

‘redis-sentinel’

A cache that stores the items in a Redis Sentinel, which is a high availability ‘load-balancer’ for a Redis cluster.

Just like for ‘redis’, if you use the same Redis database for other purposes too, then you are strongly advised to specify the CACHE_KEY_PREFIX, so keys would not accidentally collide and cache.clean() calls would only remove keys from the cache and not other records.

Example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'redissentinel'
        'CACHE_EVICTION_STRATEGY': 'time-based',
        'CACHE_REDIS_SENTINELS': [("127.0.0.1", 26379),
                                 ("10.0.0.1", 26379)],
        'CACHE_REDIS_SENTINEL_MASTER': 'mymaster',  # default: None
        'CACHE_REDIS_PASSWORD': 'MyRedisPassword',  # default: None
        'CACHE_REDIS_SENTINEL_PASSWORD': 'MyPsw',   # default: None
        'CACHE_REDIS_DB': 0,  # default: 0
        'CACHE_KEY_PREFIX': 'mycache'  # default: None
    })

‘memcached’

A cache that stores the items in a Memcached instance or cluster. It supports the pylibmc, memcache and the google app engine memcache libraries.

You can supply one or more server addresses via CACHE_MEMCACHED_SERVERS or you can supply an already initialized client, an object that resembles the API of a memcache.Client. If you have supplied a server(s) address, then the library will pick the best memcached client library available to use.

Example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'memcached',
        'CACHE_EVICTION_STRATEGY': 'time-based',
        'CACHE_MEMCACHED_SERVERS': ["127.0.0.1:11211",
                                    "127.0.0.1:11212"]
        'CACHE_KEY_PREFIX': 'cache'  # default: None
    })

Note

Flask-Caching does not pass additional configuration options to memcached backends. To add additional configuration to these caches, directly set the configuration options on the object after instantiation:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'memcached',
        'CACHE_EVICTION_STRATEGY': 'time-based',
        'CACHE_MEMCACHED_SERVERS': ["127.0.0.1:11211",
                                    "127.0.0.1:11212"]
        'CACHE_KEY_PREFIX': 'cache'  # default: None
    })

# Break convention and set options on the _client object
# directly. For pylibmc behaviors:
cache.cache._client.behaviors["tcp_nodelay"] = True

‘saslmemcached’

A cache that stores the items in an SASL-authentication protected Memcached instance or cluster.

Just like for memcached - you can supply one or more server addresses via CACHE_MEMCACHED_SERVERS or you can supply an already initialized client, an object that resembles the API of a memcache.Client.

Example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'saslmemcached',
        'CACHE_EVICTION_STRATEGY': 'time-based',
        'CACHE_MEMCACHED_SERVERS': ["127.0.0.1:11211",
                                    "127.0.0.1:11212"]
        'CACHE_MEMCACHED_USERNAME': 'myuser',  # default: None
        'CACHE_MEMCACHED_PASSWORD': 'MyPassword',  # default: None
        'CACHE_KEY_PREFIX': 'cache'  # default: None
    })

‘spreadsaslmemcached’

A subclass of the saslmemcached backend that will spread the cached values across multiple records if they are bigger than the memcached treshold which by default is 1M.

Spreading requires using pickle to store the value, which can significantly impact the performance.

‘uwsgi’

Implements the cache using uWSGI’s caching framework.

To set the uwsgi caching instance to connect to, for example: mycache@localhost:3031, use the CACHE_UWSGI_NAME argument, which defaults to an empty string, in which case uWSGI will cache in the local instance.

This backend cannot be used when running under PyPy, because the uWSGI API implementation for PyPy is lacking the required functionality.

Example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'uwsgi',
        'CACHE_UWSGI_NAME': 'mycache@localhost:3031',  # default: ''
        'CACHE_KEY_PREFIX': 'cache'  # default: None
    })

What gets cached

You might ask the question that what (what data) is getting cached when a responder is cached.

By default two things are cached: the response body and the response’s Content-Type header.

To be able to store these two things in the cache backend under one object, we use msgpack to serialize and then deserialize when loading the record back from the cache. While msgpack is a fast serializer, this does take some time.

Note

If you know that all of your cached responders are using the `Content-Type`= `application/json` header - which is very typical for basic APIs in these days - then you don’t need the `Content-Type` header to be cached. This is because the `Content-Type` = `application/json` is the default in Falcon and it is added to the response when no other value is specified.

So in case your application only generates responses with the `Content-Type` = `application/json header, then you can turn off this serialization storing the Content-Type header and benefit from the performance boost of not needing to serialize and deserialize messages.

You can turn off the serialization by setting `CACHE_CONTENT_TYPE_JSON_ONLY = True` in the config - see Configuring Falcon-Caching.

New in version 0.2.

Memoization

New in version 0.3.

See Cache.memoize()

Using the @memoize decorator you are able to cache the result of other non-view related functions. In memoization, the functions arguments are also included into the cache_key.

Note

Credits must be given to the authors and maintainers of the Flask-Caching library, as much of the code of our memoize method was ported from their popular library.

Outside just simple function, memoize is also designed for methods, since it will take into account the identity. of the ‘self’ or ‘cls’ argument as part of the cache key.

The theory behind memoization is that if you have a function you need to call several times in one request, it would only be calculated the first time that function is called with those arguments. For example, an sqlalchemy object that determines if a user has a role. You might need to call this function many times during a single request. To keep from hitting the database every time this information is needed you might do something like the following:

class Person(db.Model):
    @cache.memoize(50)
    def has_membership(self, role_id):
        return Group.query.filter_by(user=self, role_id=role_id).count() >= 1

Warning

Using mutable objects (classes, etc) as part of the cache key can become tricky. It is suggested to not pass in an object instance into a memoized function. However, the memoize does perform a repr() on the passed in arguments so that if the object has a __repr__ function that returns a uniquely identifying string for that object, that will be used as part of the cache key.

For example, an sqlalchemy person object that returns the database id as part of the unique identifier:

class Person(db.Model):
    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, self.id)

Deleting memoize cache

See Cache.delete_memoized()

New in version 0.3.

You might need to delete the cache on a per-function bases. Using the above example, lets say you change the users permissions and assign them to a role, but now you need to re-calculate if they have certain memberships or not. You can do this with the delete_memoized() function:

cache.delete_memoized(user_has_membership)

Note

If only the function name is given as parameter, all the memoized versions of it will be invalidated. However, you can delete specific cache by providing the same parameter values as when caching. In following example only the user-role cache is deleted:

user_has_membership('demo', 'admin')
user_has_membership('demo', 'user')

cache.delete_memoized(user_has_membership, 'demo', 'user')

Configuring Falcon-Caching

The following configuration values exist for Falcon-Caching:

CACHE_EVICTION_STRATEGY

The eviction strategy determines when a cached resource is removed from cache.

Available eviction strategies:

  • time-based: records are removed once time expires (default)
  • rest-based: records are removed once a PUT/POST/PATCH/DELETE call is made against the resource
  • rest-and-time-based: records are removed either by time or request method (whichever happens first)

See more at Eviction strategies

CACHE_TYPE

Specifies which type of caching object to use. This is an import string that will be imported and instantiated. It is assumed that the import object is a function that will return a cache object that adheres to the cache API.

For falcon_caching.backends objects, you do not need to specify the entire import string, just one of the following names.

Built-in cache types:

  • null: NullCache (default)
  • simple: SimpleCache
  • filesystem: FileSystemCache
  • redis: RedisCache (redis required)
  • redissentinel: RedisSentinelCache (redis required)
  • uwsgi: UWSGICache (uwsgi required)
  • memcached: MemcachedCache (pylibmc or memcache required)
  • gaememcached: same as memcached (for backwards compatibility)
  • saslmemcached: SASLMemcachedCache (pylibmc required)
  • spreadsaslmemcached: SpreadSASLMemcachedCache (pylibmc required)
CACHE_CONTENT_TYPE_JSON_ONLY Set to True if all your cached responders use the application/json Content-Type, which will turn off serialization and provide a performance boost. Defaults to False.
CACHE_NO_NULL_WARNING Silence the warning message when using cache type of ‘null’.
CACHE_ARGS Optional list to unpack and pass during the cache class instantiation.
CACHE_OPTIONS Optional dictionary to pass during the cache class instantiation.
CACHE_DEFAULT_TIMEOUT The default timeout that is used if no timeout is specified. Unit of time is seconds.
CACHE_IGNORE_ERRORS If set to any errors that occurred during the deletion process will be ignored. However, if it is set to False it will stop on the first error. This option is only relevant for the backends filesystem and simple. Defaults to False.
CACHE_THRESHOLD The maximum number of items the cache will store before it starts deleting some. Used only for SimpleCache and FileSystemCache
CACHE_KEY_PREFIX A prefix that is added before all keys. This makes it possible to use the same memcached server for different apps. Used only for RedisCache and MemcachedCache
CACHE_UWSGI_NAME The name of the uwsgi caching instance to connect to, for example: mycache@localhost:3031, defaults to an empty string, which means uWSGI will cache in the local instance.
CACHE_MEMCACHED_SERVERS A list or a tuple of server addresses. Used only for MemcachedCache
CACHE_MEMCACHED_USERNAME Username for SASL authentication with memcached. Used only for SASLMemcachedCache
CACHE_MEMCACHED_PASSWORD Password for SASL authentication with memcached. Used only for SASLMemcachedCache
CACHE_REDIS_HOST A Redis server host. Used only for RedisCache.
CACHE_REDIS_PORT A Redis server port. Default is 6379. Used only for RedisCache.
CACHE_REDIS_PASSWORD A Redis password for server. Used only for RedisCache and RedisSentinelCache.
CACHE_REDIS_DB A Redis db (zero-based number index). Default is 0. Used only for RedisCache and RedisSentinelCache.
CACHE_REDIS_SENTINELS A list or a tuple of Redis sentinel addresses. Used only for RedisSentinelCache.
CACHE_REDIS_SENTINEL_MASTER The name of the master server in a sentinel configuration. Used only for RedisSentinelCache.
CACHE_DIR Directory to store cache. Used only for FileSystemCache.
CACHE_REDIS_URL URL to connect to Redis server. Example redis://user:password@localhost:6379/2. Supports protocols redis://, rediss:// (redis over TLS) and unix://. See more info about URL support at http://redis-py.readthedocs.io/en/latest/index.html#redis.ConnectionPool.from_url. Used only for RedisCache.

Resource level caching

In Falcon-Caching you mark individual methods or resources to be cached by adding the @cache.cached() decorator to them.

It is possible to add this decorator on the resource (class) level to mark the whole resource - and so all of its 'on_' methods - as cached:

# mark the whole resource as cached
# which will decorate all the on_...() methods of this class
@cache.cached(timeout=600)
class ThingsResource:

    def on_get(self, req, resp):
        pass

    def on_post(self, req, resp):
        pass

BUT if any of those 'on_' methods are supposed to modify the data or have some other non-cachable actions, then that will NOT be executed when the response is returned from the cache - assuming the CACHE_EVICTION_STRATEGY is set to ‘time-based’ - which is the default.

The CACHE_EVICTION_STRATEGY values of ‘rest-based’ and ‘rest-and-time-based’ are safe, as those invalidate the cache for any PUT/PATCH/POST/DELETE calls and do NOT serve the response from the cache for those methods.

This happens because the cache.middleware short-circuits any further processing if a cached version of that item is found. If a cached version is found then it will skip any remaining process_request and process_resource methods, as well as the responder method that the request would have been routed to. However, any process_response middleware methods will still be called.

We suggest that you only use the resource level (eg class) decorator if you use the CACHE_EVICTION_STRATEGY of ‘rest-based’ or ‘rest-and-time-based’ and NOT if you use the ‘time-based’ strategy. The only exception to this rule of thumb could be if (1) you are certain that all the methods of that resource can be served from the cache or (2) all the actions for those methods are taken in process_response phase.

Explicitly Caching Data

Data can be cached explicitly by using the proxy methods like Cache.set(), and Cache.get() directly. There are many other proxy methods available via the Cache class - see them listed below.

For example:

from falcon_caching import Cache

cache = Cache(
    config={
        'CACHE_TYPE': 'simple',
        'CACHE_EVICTION_STRATEGY': 'rest-based'
    })

...

def test(foo=None):
    if foo is not None:
        cache.set("foo", foo)  # saving a value into the cache
    bar = cache.get("foo")  # retrieving the value from the cache

Supported methods:

cache.set("foo", "bar")
cache.has("foo")
cache.get("foo")
cache.add("foo", "bar")  # like set, except it doesn't overwrite
cache.set_many({"foo": "bar", "foo2": "bar2"})
cache.get_many(["foo", "foo2"])  # returns a list
cache.get_dict(["foo", "foo2"])  # returns a dict
cache.delete("foo")
cache.delete_many("foo", "foo2")
cache.set("foo3", 1)
cache.inc("foo3")  # increment, only supported by Redis&Redis Sentinel
cache.dec("foo3")  # decrement, only supported by Redis&Redis Sentinel
cache.clear()  # clears all cache - not supported by all backends
               # WARNING: some implementations (Redis) will flush
               # the whole database!!!

Query String

Currently the query string is NOT used in the cache key, so two requests which only differ in the query string will be cached against the same key.

Recipes

Multiple decorators

For scenarios where there is a need for multiple decorators and the @cache.cached() cannot be the topmost one, we need to register the decorators a special way.

This scenario is complicated because our @cache.cached() just marks the fact that the given method is decorated with a cache, which later gets picked up by the middleware and triggers caching. If the @cache.cached() is the topmost decorator then it is easy to pick that up, but if there are other decorators ‘ahead’ it, then those will ‘hide’ the @cache.cached(). This is because decorators in Python are just syntactic sugar for nested function calls.

To be able to tell if the given endpoint was decorated by the @cache.cached() decorator when that is NOT the topmost decorator, you need to decorate your method by registering your decorators using the @register() helper decorator.

See more about this issue at https://stackoverflow.com/questions/3232024/introspection-to-get-decorator-names-on-a-method

import falcon
from falcon_caching import Cache
from falcon_caching.utils import register

limiter = Limiter(
    key_func=get_remote_addr,
    default_limits=["10 per hour", "2 per minute"]
)

cache = Cache(config={'CACHE_TYPE': 'simple'})

class ThingsResource:
    # this is fine, as the @cache.cached() is the topmost (eg the first) decorator:
    @cache.cached(timeout=600)
    @another_decorator
    def on_get(self, req, resp):
        pass

class ThingsResource2:
    # the @cache.cached() is NOT the topmost decorator, so
    # this would NOT work - the cache decorator would be ignored!!!!
    # DO NOT DO THIS:
    @another_decorator
    @cache.cached(timeout=600)
    def on_get(self, req, resp):
        pass

class ThingsResource3:
    # use your decorators this way:
    @register(another_decorator, cache.cached(timeout=600))
    def on_get(self, req, resp):
        pass

Development

For development guidelines see https://github.com/zoltan-fedor/falcon-caching#development

API Reference

If you are looking for information on a specific function, class or method of a service, then this part of the documentation is for you.