Caching API Responses in Flask

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.
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?
Application Level Caching.
Using an External Caching with the help of a data store like Redis.
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:
We have used Marshmallow as a simple validation layer for our sample API.
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.
Keep debug as False in production.
Let’s explore our Caching Options:
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.
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.
Local vs Centralized Caching.
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.
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,
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.
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.2
→ 192.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.
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.