Caching API Responses in Flask

Flask IndiaFlask India
15 min read

Introduction:

Flask is one of the popular choices to create RESTful APIs in Python backend development. It is easy to learn and one can come up quite quickly in terms of the same. You can have a quick overview with the help of one of our prior episodes.

  1. Learn Flask Development as a Beginner

  2. Run your Flask App

What is Caching?

Caching in web development refers to storing the precomputed API respones and sending the same as an API response.

For a simple example, lets take this example of a Python Dictionary below:

import time

def get_person_id(id):
    time.sleep(5.0)
    return f'Person-{id}'

local_cache = {
    '1': 'Person-1',
    '2': 'Person-2',
    '3': 'Person-3'
}

def person_name_cache(_id):
    if local_cache.get(_id) is not None:
        return local_cache.get(_id)

    else:
        name = get_person_id(_id)
        local_cache[_id] = name
        return name

_id = 4

# Fetched without Cache:
start = time.time()
person_name = person_name_cache(_id)
end = time.time()
print(f'Name of person with id: {_id} is {person_name} fetched in {round((end-start), 4)} secs')

# Fetched with Cache:
start = time.time()
person_name = person_name_cache(_id)
end = time.time()
print(f'Name of person with id: {_id} is {person_name} fetched in {round((end-start), 4)} secs')

You should see a similar output when we run these in our Python terminal shell.

Name of person with id: 4 is Person-4 in 5.0061 secs
Name of person with id: 4 is Person-4 in 0.0004 secs

Why do we want to cache API responses?

We now have machines with orders of magnitude more power than their predecessors, but one analogy remains the same: while the compute may be cheap, computing isn’t. Hence, it is necessary to avoid recomputing the data to be sent back to the client to save on resources wherever necessary and also decrease API response times, thereby increasing developer productivity.

What are my options?

  1. Application Level Caching.

  2. Using an External Caching with the help of a data store like Redis.

  3. Using a Reverse Proxy Setup like Nginx.

Let’s create our Flask App:

# Import Modules:

import time
import traceback
from flask import Flask, request, jsonify
from marshmallow import Schema, fields, ValidationError, validate

app = Flask(__name__)

# Marshmallow Schema:

class RangeSchema(Schema):
    start = fields.Integer(
        required=True, 
        validate=validate.Range(min=10, max=1000)
    )
    end = fields.Integer(
        required=True, 
        validate=validate.Range(min=10, max=1000)
    )

schema = RangeSchema()

@app.route('/square', methods=['POST'])
def square_numbers():
    try:
        data = schema.load(request.get_json())
        start = data['start']
        end = data['end']

        result = [n ** 2 for n in range(start, end+1)]
        time.sleep(2.0)
        return jsonify({'squares': result}), 200

    except ValidationError as err:
        traceback.print_exc()
        return jsonify({"errors": err.messages}), 400

    except Exception as err:
        traceback.print_exc()
        return jsonify({"errors": "internal server error"}), 500

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

Our Directory Structure looks like the below:

(venv) ➜  flask_caching ls -lah
total 16
drwxr-xr-x  5 flask-india@dev-pc  staff   160B Jul 26 16:57 .
drwxr-xr-x  9 flask-india@dev-pc  staff   288B Jul 26 16:56 ..
-rw-r--r--@ 1 flask-india@dev-pc  staff   1.0K Jul 26 18:39 flask_app.py
drwxr-xr-x  6 flask-india@dev-pc  staff   192B Jul 26 16:56 venv

Note:

  1. We have used Marshmallow as a simple validation layer for our sample API.

  2. We have added a sleep of 2 seconds to demonstrate a simple workload requiring some amount of time to complete. Your real-world scenario can have a Database Query, some other complex computation as well.

  3. Keep debug as False in production.

Let’s explore our Caching Options:

  1. Application Level Caching:

Although we have demonstrated with a simple module-level caching for our application, the actual scenario is a bit more complicated since there are various other factors involved as well, such as Cache Time Period, Caching Algorithm, etc.

We will use a Python package named cacheout to help us out with the same.

https://cacheout.readthedocs.io/en/latest/

We install and save our dependency as below.

(venv) ➜  flask_caching pip install cacheout

Collecting cacheout
  Downloading cacheout-0.16.0-py3-none-any.whl (21 kB)
Installing collected packages: cacheout
Successfully installed cacheout-0.16.0
WARNING: You are using pip version 21.2.4; however, version 25.1.1 is available.
You should consider upgrading via the '/Users/flask-india@dev-pc/Desktop/flask_india_blog/articles/flask_caching/venv/bin/python3 -m pip install --upgrade pip' command.
(venv) ➜  flask_caching 
(venv) ➜  flask_caching pip freeze > requirements.txt

Our directory structure now:

(venv) ➜  flask_caching ls -lah
total 16
drwxr-xr-x  5 flask-india@dev-pc  staff   160B Jul 26 16:57 .
drwxr-xr-x  9 flask-india@dev-pc  staff   288B Jul 26 16:56 ..
-rw-r--r--@ 1 flask-india@dev-pc  staff   1.0K Jul 26 18:39 flask_app.py
-rw-r--r--  1 flask-india@dev-pc  staff   250B Jul 26 19:24 requirements.txt
drwxr-xr-x  6 flask-india@dev-pc  staff   192B Jul 26 16:56 venv

Our requirements.txt file is as follows:

(venv) ➜  flask_caching cat requirements.txt 
backports-datetime-fromisoformat==2.0.3
blinker==1.9.0
cacheout==0.16.0
click==8.1.8
Flask==3.1.1
importlib_metadata==8.7.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
marshmallow==4.0.0
typing_extensions==4.14.1
Werkzeug==3.1.3
zipp==3.23.0

Let’s modify our app and add the package that we have installed now.

# Import Modules:

import time
import traceback
from flask import Flask, request, jsonify
from marshmallow import Schema, fields, ValidationError, validate
from cacheout import Cache

app = Flask(__name__)

app_cache = Cache(maxsize=50240, ttl=60)  # ttl in seconds

def set_to_cache(key, value):
    try:
        app_cache.set(key, value)

    except Exception:
        traceback.print_exc()

def get_from_cache(key):
    try:
        if app_cache.has(key):
            return True, app_cache.get(key)
        return False, None

    except Exception:
        traceback.print_exc()
        return False, None


# Marshmallow Schema:

class RangeSchema(Schema):
    start = fields.Integer(
        required=True, 
        validate=validate.Range(min=10, max=1000)
    )
    end = fields.Integer(
        required=True, 
        validate=validate.Range(min=10, max=1000)
    )

schema = RangeSchema()

@app.route('/square', methods=['POST'])
def square_numbers():
    try:
        data = schema.load(request.get_json())
        start = data['start']
        end = data['end']

        key = f'{start}-{end}'
        value_exists, value = get_from_cache(key)

        if value_exists == True:
            return jsonify({'squares': value}), 200

        else:
            result = [n ** 2 for n in range(start, end+1)]
            set_to_cache(key, result)
            time.sleep(2.0)

        return jsonify({'squares': result}), 200

    except ValidationError as err:
        traceback.print_exc()
        return jsonify({"errors": err.messages}), 400

    except Exception as err:
        traceback.print_exc()
        return jsonify({"errors": "internal server error"}), 500

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

Let’s start to test our API now, with the curl below

curl --location 'http://127.0.0.1:8000/square' \
--header 'Content-Type: application/json' \
--data '{
    "start": 20,
    "end": 130
}'

You can see the raw computation being done with our 2-second time.sleep, and hence, the API response times exceed 2 seconds.

When we hit the API a second time, we see the cache come into play, resulting in much better API response times as well.

  1. Using an external Data Store like Redis:

This is a more commonly used pattern, along with approach number 1 discussed above. You can use a combination of 1 and 2

First, we need to install the Redis server if not already installed on our system. I’m running a Redis Server via a Docker container as follows.

(venv) ➜  flask_caching sudo docker run -d --name test-redis-server -e ALLOW_EMPTY_PASSWORD=yes -p 6380:6379 bitnami/redis:latest 
fb3a005acae8617cd6e2229bbb3b250d7cd810b5210372191f66dd1760c11452

Note: I already have a Redis Server Image loaded on my system from Bitnami, which I’m reusing. The newly spawned container can be seen using the command below, and has been asked to use a new port 6380 since port 6379 is the default Redis Server port and is in use by an existing Redis Server docker container.

(venv) ➜  flask_caching sudo docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS                      PORTS                    NAMES
fb3a005acae8   bitnami/redis:latest     "/opt/bitnami/script…"   4 seconds ago   Up 3 seconds                0.0.0.0:6380->6379/tcp   test-redis-server
cd67bce15559   bitnami/redis:latest     "/opt/bitnami/script…"   2 months ago    Up 14 minutes               0.0.0.0:6379->6379/tcp   dev-redis

My Docker Images:

(venv) ➜  flask_caching sudo docker images
Password:
REPOSITORY               TAG                IMAGE ID       CREATED         SIZE
bitnami/redis            latest             333cd28208f4   2 months ago    276MB

Let’s modify our Flask App to use Redis Caching now.


# Import Modules:

import time
import traceback
from flask import Flask, request, jsonify
from marshmallow import Schema, fields, ValidationError, validate
import redis
import json

app = Flask(__name__)

# Our new local setup uses port 6380

redis_client = redis.Redis(host='localhost', port=6380, db=0)

# Marshmallow Schema:

class RangeSchema(Schema):
    start = fields.Integer(
        required=True, 
        validate=validate.Range(min=10, max=1000)
    )
    end = fields.Integer(
        required=True, 
        validate=validate.Range(min=10, max=1000)
    )

schema = RangeSchema()

@app.route('/square', methods=['POST'])
def square_numbers():
    '''
       For ease of use, we will be using Redis String Data Structure to store and 
       retrieve our data. So any complex data will need to be JSON compatible before
       storing the compute result.

       We will use Redis GET and SET operator to work with same.
    '''
    try:
        data = schema.load(request.get_json())
        start = data['start']
        end = data['end']

        cache_key = f"flask_caching:squares:{start}:{end}"
        cached_result = redis_client.get(cache_key)
        if cached_result is not None:
            return jsonify({"squares": json.loads(cached_result)}), 200

        else:
            result = [n ** 2 for n in range(start, end)]
            redis_client.set(cache_key, json.dumps(result), ex=120)  # cache for 120 secs
            time.sleep(2.0)

        return jsonify({'squares': result}), 200

    except ValidationError as err:
        traceback.print_exc()
        return jsonify({"errors": err.messages}), 400

    except Exception as err:
        traceback.print_exc()
        return jsonify({"errors": "internal server error"}), 500

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

Let’s test our app again with the curl request below:

curl --location 'http://127.0.0.1:8000/square' \
--header 'Content-Type: application/json' \
--data '{
    "start": 20,
    "end": 130
}'

You will see the response times similar to below:

Without Redis Caching:

With Redis Caching:

You can see the Redis Cached data as below:

(venv) ➜  flask_caching sudo docker exec -it test-redis-server redis-cli
Password:
127.0.0.1:6379> 
127.0.0.1:6379> keys *
1) "flask_caching:squares:20:130"
127.0.0.1:6379> 
127.0.0.1:6379> GET flask_caching:squares:20:130
"[400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801, 10000, 10201, 10404, 10609, 10816, 11025, 11236, 11449, 11664, 11881, 12100, 12321, 12544, 12769, 12996, 13225, 13456, 13689, 13924, 14161, 14400, 14641, 14884, 15129, 15376, 15625, 15876, 16129, 16384, 16641]"

Note: We use the Redis client provided inside the Redis Server Docker Image. Also, internally, the Redis Server runs on port 6379 only, as evident from the Redis Client above.

We can check the TTL (Time to Live) of the key, indicating its expiry as well.

127.0.0.1:6379> keys *
1) "flask_caching:squares:20:130"
127.0.0.1:6379> TTL flask_caching:squares:20:130
(integer) 99

Note: You may see similar response times while using Redis Server as a Caching Data Store vs using Application Memory. However, there are a few differences here.

  1. Local vs Centralized Caching.

  2. No Network Round Trip when using in-app memory for caching vs 1 extra Network Round Trip used when using Redis Server as a Centralized store.

You should use a mix of the above or any one, depending on your use case.

  1. Using Nginx as a Reverse Proxy for Caching:

What is Nginx?

nginx ("engine x") is an HTTP web server, reverse proxy, content cache, load balancer, TCP/UDP proxy server, and mail proxy server.

From the docs, you can review the same here below,

Nginx Website

Why do we need Nginx?

In a real-world scenario, we do not expose our Flask app directly to the internet for security reasons. We run it in an isolated environment and only allow necessary traffic inbound and outbound to our app.

Nginx is one of the popular choices out there to run in front of our app to do the same.

We will set up Nginx as a Docker container on our local setup using the command below.

(venv) ➜  flask_caching sudo docker run -d --name dev-nginx -p 80:80 nginx:latest
Password:
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
10d3f5dcba63: Download complete 
7996b9ca9891: Download complete 
b3407f3b5b5b: Download complete 
5850200e50af: Download complete 
6d01b3c42c10: Download complete 
a3bfeb063ded: Download complete 
ecab78f9d45d: Download complete 
Digest: sha256:84ec966e61a8c7846f509da7eb081c55c1d56817448728924a87ab32f12a72fb
Status: Downloaded newer image for nginx:latest
b9a12e364e985654acbc0c3babf1c471a61cc044c80d5394d3b9a1b587ce0e9f

Let’s check our running Docker container below.

(venv) ➜  flask_caching sudo docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS                      PORTS                    NAMES
b9a12e364e98   nginx:latest             "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes                0.0.0.0:80->80/tcp       dev-nginx
fb3a005acae8   bitnami/redis:latest     "/opt/bitnami/script…"   2 hours ago     Up 2 hours                  0.0.0.0:6380->6379/tcp   test-redis-server
cd67bce15559   bitnami/redis:latest     "/opt/bitnami/script…"   2 months ago    Up 2 hours                  0.0.0.0:6379->6379/tcp   dev-redis

You can verify your setup by going to the URL below in your browser. Port 80 is not to be specified and is considered as default by our internet browsers.

http://localhost/

You should see a screenshot as below to confirm the same.

Now let’s modify our Nginx config to re-route our traffic from port 80 to our Flask App. We will copy the Nginx config from inside the container to outside, so we can update the same.

➜  flask_caching sudo docker cp dev-nginx:/etc/nginx/conf.d/default.conf .
Successfully copied 3.07kB to /Users/flask-india@dev-pc/Desktop/flask_india_blog/articles/flask_caching/.

Note: The Default configuration for the Nginx Web Server is located at /etc/nginx/conf.d/ directory. We are modifying same for our tutorial purposes.

Current Directory:

➜  flask_caching ls -lah
total 24
drwxr-xr-x  6 flask-india@dev-pc  staff   192B Jul 27 18:23 .
drwxr-xr-x  9 flask-india@dev-pc  staff   288B Jul 26 16:56 ..
-rw-r--r--@ 1 root                staff   1.1K Jul 27 18:01 default.conf
-rw-r--r--@ 1 flask-india@dev-pc  staff   2.3K Jul 27 16:56 flask_app.py
-rw-r--r--  1 flask-india@dev-pc  staff   284B Jul 27 15:18 requirements.txt
drwxr-xr-x  6 flask-india@dev-pc  staff   192B Jul 26 16:56 venv

Current default.conf file:

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

Modified default.conf file:

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /square {
        proxy_pass http://192.168.0.115:8000/square;

        # Add caching headers
        add_header Cache-Control "public, max-age=3600";

        # Include original response headers from Flask API
        proxy_pass_request_headers on;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

I have checked my system IP using the Flask App bootup debug log below.

(venv) ➜  flask_caching python flask_app.py 
 * Serving Flask app 'flask_app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8000
 * Running on http://192.168.0.115:8000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 169-471-056

You can also check the same via the ifconfig command, check my output below.

➜  flask_caching sudo ifconfig
Password:
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
anpi1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 36:80:a8:fc:35:5d
    media: none
    status: inactive
anpi0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 36:80:a8:fc:35:5c
    media: none
    status: inactive
en3: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 36:80:a8:fc:35:3c
    nd6 options=201<PERFORMNUD,DAD>
    media: none
    status: inactive
en4: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 36:80:a8:fc:35:3d
    nd6 options=201<PERFORMNUD,DAD>
    media: none
    status: inactive
en1: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500
    options=460<TSO4,TSO6,CHANNEL_IO>
    ether 36:e6:e0:68:f9:00
    media: autoselect <full-duplex>
    status: inactive
en2: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500
    options=460<TSO4,TSO6,CHANNEL_IO>
    ether 36:e6:e0:68:f9:04
    media: autoselect <full-duplex>
    status: inactive
ap1: flags=8802<BROADCAST,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 72:a6:d8:de:7d:0f
    media: autoselect
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=6460<TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
    ether 50:a6:d8:de:7d:0f
    inet6 fe80::10c6:da6b:f436:5436%en0 prefixlen 64 secured scopeid 0xc 
    inet 192.168.0.115 netmask 0xffffff00 broadcast 192.168.0.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
awdl0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=6460<TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
    ether 9e:01:34:9f:4b:f7
    inet6 fe80::9c01:34ff:fe9f:4bf7%awdl0 prefixlen 64 scopeid 0xd 
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
llw0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 9e:01:34:9f:4b:f7
    inet6 fe80::9c01:34ff:fe9f:4bf7%llw0 prefixlen 64 scopeid 0xe 
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: inactive
bridge0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=63<RXCSUM,TXCSUM,TSO4,TSO6>
    ether 36:e6:e0:68:f9:00
    Configuration:
        id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
        maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
        root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
        ipfilter disabled flags 0x0
    member: en1 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 8 priority 0 path cost 0
    member: en2 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 9 priority 0 path cost 0
    nd6 options=201<PERFORMNUD,DAD>
    media: <unknown type>
    status: inactive
utun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
    inet6 fe80::f816:afa6:7cd0:fb54%utun0 prefixlen 64 scopeid 0x10 
    nd6 options=201<PERFORMNUD,DAD>
utun1: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1380
    inet6 fe80::e4c3:d56a:eee9:6765%utun1 prefixlen 64 scopeid 0x11 
    nd6 options=201<PERFORMNUD,DAD>
utun2: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 2000
    inet6 fe80::cc3a:554a:ecdc:8083%utun2 prefixlen 64 scopeid 0x12 
    nd6 options=201<PERFORMNUD,DAD>
utun3: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1000
    inet6 fe80::ce81:b1c:bd2c:69e%utun3 prefixlen 64 scopeid 0x13 
    nd6 options=201<PERFORMNUD,DAD>
➜  flask_caching

You need to check the local IP range in a series of 192.168.1.2192.168.1.254, since 192.168.0.1 and 192.168.0.155 are broadcast IP addresses for the network range where the Router's default gateway 192.168.0.1

Now let’s move the updated config file back to the Nginx Docker Container and verify the same.

➜  flask_caching sudo docker cp default.conf dev-nginx:/etc/nginx/conf.d/default.conf
Password:
Successfully copied 3.07kB to dev-nginx:/etc/nginx/conf.d/default.conf
➜  flask_caching sudo docker exec -it dev-nginx bash                                 
root@b9a12e364e98:/# cat /etc/nginx/conf.d/default.conf 
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /square {
        proxy_pass http://192.168.0.115:8000/square;

        # Add caching headers
        add_header Cache-Control "public, max-age=3600";

        # Include original response headers from Flask API
        proxy_pass_request_headers on;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

root@b9a12e364e98:/#

Let’s reload our Nginx config and restart our proxy server.

root@b9a12e364e98:/# nginx -t 
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
root@b9a12e364e98:/# 
root@b9a12e364e98:/# nginx -s reload
2025/07/27 13:38:46 [notice] 89#89: signal process started
root@b9a12e364e98:/# exit
exit

What's next:
    Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug dev-nginx
    Learn more at https://docs.docker.com/go/debug-cli/
➜  flask_caching

Let’s test our REST API on port 80 in Postman and see the results.

curl --location 'http://localhost:80/square' \
--header 'Content-Type: application/json' \
--data '{
    "start": 20,
    "end": 130
}'

Since the Postman doesn't cache responses as per its design, we need to inspect the headers we have set via our Nginx Proxy Server setup. Let’s check the headers section for the same.

You can see the cache-control header in the screenshot above. This will come into play when this API response is being used by the browser.

Conclusion:

We have seen how to implement caching in our Flask API, allowing us to conserve precious compute resources and provide a better overall user experience to our end users. We have explored 3 detailed approaches with detailed examples for first timers to easily understand the same. There are different caching strategies as per different architectural considerations, which can be explored later as a part of an advanced tutorial.

0
Subscribe to my newsletter

Read articles from Flask India directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Flask India
Flask India

We are a bunch of Developers who started to mentor beginners who started using Python and Flask in general in the Flask India Telegram Group. So this blog is a way to give it back to the community from where we have received guidance when we started as beginners.