Implement API Caching with Redis, Flask and Docker [Step-By-Step]

Implement API Caching with Redis, Flask and Docker [Step-By-Step]

Do you want your API to be faster, and more consistent, and to reduce the requests to the server? — That’s where caching comes into play. In this article, I will show you how to implement API Caching with Redis on Flask. I am taking Flask as an example here, but the concept of Caching is the same regardless of the technologies.

  • The following updates were made in February 2022 — Python was updated to 3.9 and changed the project structure

What’s caching?

Before we move into the practical part about implementing Caching with Redis and Flask, let’s first know what’s caching as a definition and learn it as a concept so you know then what would the use cases be.

Caching is the ability to store copies of frequently accessed data in several places along the request-response path. When a consumer requests a resource representation, the request goes through a cache or a series of caches (local cache, proxy cache, or reverse proxy) toward the service hosting the resource. If any of the caches along the request path has a fresh copy of the requested representation, it uses that copy to satisfy the request. If none of the caches can satisfy the request, the request travels to the service (or the origin server as it is formally known). This is well defined with two terminologies, which are cache miss and cache hit.

Cache hit — A cache hit is a state in which data requested for processing by a component or application is found in the cache memory. It is a faster means of delivering data to the processor, as the cache already contains the requested data.

Cache miss Cache miss is a state where the data requested for processing by a component or application is not found in the cache memory. It causes execution delays by requiring the program or application to fetch the data from other cache levels or the main memory.

As mentioned above, there are several ways to implement caching. That can be on the client side through Web Caching, on the server side through Data Caching (Relational Databases, Redis, etc), and Application Caching through plugins that get installed on the application (ex: plugins on WordPress). For this tutorial we’re going to use Redis, to save the responses from the API, and then use those responses instead of making requests to the server to fetch the data.

Flask and Redis — Implementation

Prerequisites:

  • Docker & Docker-compose

  • Flask

  • Python 3.*+

We are going to use docker to isolate our services, and then docker-compose to orchestrate the services together (putting them on the same network, communication between them, environment variables, etc). If you don’t know about Docker, I suggest you refer to the official docs here.

General workflow

Project setup:

Create python virtualenv and install Flask, Redis, flask-caching, and requests:

python -m venv venv
source venv/bin/activate
pip install Flask redis flask_caching requests

Our application will look something like this:

/root
├── app.py                 - Application entrypoint
├── config.py              - Config file for Flask
├── docker-compose.yml     - Docker compose for app and redis
├── Dockerfile             - Dockerfile for Flask API
├── .env                   - Environment variables

So let's go ahead and create files that are necessary for this setup:

touch Dockerfile docker-compose.yml .env
pip freeze > requirements.txt
touch config.py app.py

What we are going to implement?

We are just going to make a simple endpoint that fetches the university data from Hipolabs universities API and based on the country that we sent as a query parameter, we get a list with the universities for the specified country.

Let’s go ahead and in app.py create an instance of Flask, and use that to create an endpoint that fetches university data.

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)


@app.route("/universities")
def get_universities():
    API_URL = "http://universities.hipolabs.com/search?country="
    search = request.args.get('country')
    r = requests.get(f"{API_URL}{search}")
    return jsonify(r.json())


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=5000)

So basically, based on the query parameter country it makes the request to the external API and gets back the data in JSON format . Let’s go ahead and try it:

export FLASK_APP=app.py      # To tell where your flask app lives
export FLASK_ENV=development # Set debug mode on
flask run

I will be using Postman to make the request because I also want to see the time that my request takes to process.

Testing the endpoint with Postman; fetch all the universities for Germany

Okay, now we see that we have the results, and everything is working fine as excepted. With the red color, you can see the time that it took to get the data from that endpoint. We can try and make the same request several times, and the performance won’t change. That is because we’re always making a new request to the server. Our goal is to minimize this, and as explained at the beginning to make fewer requests to the server. So let’s go ahead and do that.


Add Redis and dockerize the application

We saw that it worked fine locally, but now we want to implement caching, and for that, we’re going to need Redis. There are several approaches you can take here as:

  • Installing Redis (Officially compatible in Linux, not in Windows, see here)

  • Host a Redis instance and use that one (ex: Redis instance on Heroku)

  • Start the Redis instance with Docker (We are doing this)

We are going to dockerize the application and add Redis as a service so we can easily communicate from our application. Let’s go ahead and write the Dockerfile for the Flask application:

FROM python:3.7
# Run commands from /app directory inside container
WORKDIR /app
# Copy requirements from local to docker image
COPY requirements.txt /app
# Install the dependencies in the docker image
RUN pip3 install -r requirements.txt --no-cache-dir
# Copy everything from the current dir to the image
COPY . .

We don’t have a command here to run the image, as I will use docker-compose to run the containers. Let’s configure docker-compose to run our application and Redis:


version: '3'
services:
  api:
    container_name: flask-container
    build: .
    entrypoint: python app.py
    env_file:
      - .env
    ports:
      - '5000:5000'
  redis:
    image: redis
    container_name: redis-container
    ports:
      - "6379:6379"

So we simply add two services, which are our application and Redis. For the application, we expose the port 5000in and out, and for Redis, we expose 6379. Now let’s start the services with docker-compose.

docker-compose up -d --build

Our services should be up and running, and if we go again and make the same request as we did above when we were running the application without Docker, we will have the same output. To check if the services are running enter the following command:

docker ps

Now let’s configure our application to connect with the Redis instance, and also to implement caching in our endpoint. We can go straight and set the variables directly in the code, but here I am trying to show you some good practices while developing with Flask and Docker. In the docker-compose from the above gist, we can see that for the environment variables I refer to the .env file, and then I use config.py to map these variables to the Flask application. For the flask-caching library to work, we need to set some environment variables, which are for Redis connection and caching type. You can read more about the configuration from the documentation of the library, based on the caching type that you want to implement.

# .env file
CACHE_TYPE=redis
CACHE_REDIS_HOST=redis
CACHE_REDIS_PORT=6379
CACHE_REDIS_DB=0
CACHE_REDIS_URL=redis://redis:6379/0
CACHE_DEFAULT_TIMEOUT=500

In the .env we set some variables like caching type, host, DB, etc. Since we have these variables mounted from docker-compose inside our container, now we can get those variables using the os module. Let’s get those variables in config.py and we’ll use them later to map the values to our Flask application.

import os

class BaseConfig(object):
    CACHE_TYPE = os.environ['CACHE_TYPE']
    CACHE_REDIS_HOST = os.environ['CACHE_REDIS_HOST']
    CACHE_REDIS_PORT = os.environ['CACHE_REDIS_PORT']
    CACHE_REDIS_DB = os.environ['CACHE_REDIS_DB']
    CACHE_REDIS_URL = os.environ['CACHE_REDIS_URL']
    CACHE_DEFAULT_TIMEOUT = os.environ['CACHE_DEFAULT_TIMEOUT']

From the configuration side of things, we’re good. Now let’s initialize the cache on top of Flask and integrate that with our application.

import requests
from flask import Flask, jsonify, request
from flask_caching import Cache  # Import Cache from flask_caching module

app = Flask(__name__)
app.config.from_object('config.Config')  # Set the configuration variables to the flask application
cache = Cache(app)  # Initialize Cache


@app.route("/universities")
@cache.cached(timeout=30, query_string=True)
def get_universities():
    API_URL = "http://universities.hipolabs.com/search?country="
    search = request.args.get('country')
    r = requests.get(f"{API_URL}{search}")
    return jsonify(r.json())

We have added a new decorator which is @cache.cached then we specify a timeout which is the time that this response will be cached in Redis memory. So basically after the first request, we will have this response stored for 30 seconds, after that there’ll be a fresh request that will update the memory again. The second parameter query_string=True which in this case makes sense because we want to store the responses based on the query string that we store instead of the static path.

  • query_string — Default False. When True, the cache key used will be the result of hashing the ordered query string parameters. This avoids creating different caches for the same query just because the parameters were passed in a different order.

And we’re done, let’s build the containers again and test this out in action with caching in place.

docker-compose up -d --build

Now let’s go to Postman again, and do the same request on the universities endpoint.

Response time after implementing caching with Redis

For the first time, we’ll have approximately the same time as we did when we weren’t using caching, but if we do the same request again, we’ll have significant improvements and all that thanks to Redis. So what we’re doing is, we are saving the response to an in-memory database, and then while the data are still stored there they’ll be returned from there, instead of making the request from the server.


Dive deeper? — Let us see that in action, by using a GUI tool, to query our Redis store. I am using TablePlus, for the sake of the visualization, but you can also use Redis CLI to query the data. To connect to our Redis instance we will specify the host as localhost and then for the port, we enter 6379 just as we were exposed in docker-compose .

After that, we can see the data that are being stored in our Redis instance. When there’s a response saved, you can see a db0 and if we look for more, we’ll see our cached response including [key; value; type; ttl].

We can clearly see that the response that is cached is /universities?* and that is available for the time that appears on the ttl. This section was a bit outside of the scope, but it’s good to know what is happening in the background.

So with that, we have implemented API caching with Redis and Flask. For more options please refer to the documentation of the flask-caching library which is a wrapper to implement caching around different clients.

Conclusions

So we implemented API Caching using Redis. This is a simple example, but it includes lots of details regarding this topic. Caching is really important when you write applications, as it helps a lot with the performance, and when possible, you should implement it, but make sure that you’re targeting the right use case.

You can find the full source code of the article on the GitHub repository, with the instructions.

vjanz/flask-cache-redis

If you found it helpful, please don’t forget to clap & share it on your social network or with your friends.

If you have any questions, feel free to reach out to me.

If you would like to support my work, you can buy me a coffee by clicking the image below 😄

Connect with me on LinkedIn, GitHub

References:

https://www.cloudflare.com/learning/cdn/what-is-caching/
redislabs.com
docs.docker.com
flask-caching.readthedocs.io
universities.hipolabs.com