Caching is one of the simplest ways to make APIs feel instant. If your responses don’t change every millisecond, you can let Nginx serve them straight from memory or disk instead of waking your backend server up each time.
Why cache API responses?
Benefits
- Lower latency: Hits served from memory or disk are much faster than recomputing in the app.
- Higher throughput: Offload repetitive requests from Odoo, freeing workers for real work.
- Cost control: Fewer CPU cycles and database queries.
- Graceful under load: Serve stale responses when the upstream is slow or down.
Great candidates for caching
- Public, read-only endpoints (e.g., product catalogs, blog posts, listings, price boards with modest freshness needs).
- Data with natural TTLs (e.g., “top sellers in the last hour”).
- Expensive queries that don’t change every second.
Usually avoid caching
- Per-user or personalized endpoints (unless you vary by user and accept the memory cost).
- Highly dynamic or transactional data (cart totals, wallet balances).
- Sensitive or private data (auth required) unless you explicitly scope the cache key per user and mark it private.
Odoo Controller
Here’s a minimal controller for testing purposes that exposes contact data (res.partner) at /api/contact and makes it cache-friendly.
# -*- coding: utf-8 -*-
from odoo.http import Controller, request, route
import json
class OdooRestAPI(Controller):
"""
Odoo Rest API Controller.
"""
@route(
['/api/contact'], type='http', auth='none', csrf=False,
methods=['GET'], cors='*')
def odoo_rest_api(self, **kw):
"""
Fetch Data.
"""
try:
data = request.env['res.partner'].sudo().search_read(
fields=[
'id', 'name', 'parent_name', 'child_ids',
'lang', 'website', 'active', 'street',
'street2', 'zip', 'city', 'state_id',
'country_id', 'email', 'phone'
]
)
return request.make_response(
json.dumps(data),
status=200,
headers={'Cache-Control': 'public, max-age=60'}
)
except Exception as e:
return request.make_response(
"Something went wrong",
status=500
)
What this code does
- Defines a /api/contact route: Accessible publicly, no auth required, GET only.
- Fetches all res.partner records (Odoo Contacts).
- Returns JSON with a Cache-Control: public, max-age=60 header so responses can be cached for 60 seconds by Nginx or a browser.
- Handles errors gracefully with a 500 fallback.
Caching with NGINX
In our case, caching means that NGINX will keep a copy of the web server’s response on its own server. When the same request comes in again, NGINX doesn’t need to bother the backend (Odoo, in our example). Instead, it serves the stored response directly to the client. This reduces the amount of work the backend has to do and makes responses much faster.
To enable this behavior, we’ll update the /etc/nginx/sites-available/odoo configuration file with some additional settings.
Create a directory for storing the cached response.
sudo mkdir /var/cache/nginx/odoo
# Caching Path
proxy_cache_path /var/cache/nginx/odoo levels=1:2 keys_zone=odoo_cache:10m inactive=60m;
server {
listen 443;
# Cache idempotent urls.
location ~ ^/api/[a-z]+$ {
proxy_pass http://127.0.0.1:8028;
proxy_cache odoo_cache;
proxy_cache_valid any 10m;
add_header X-Proxy-Cache $upstream_cache_status;
}
# The rest of the request is not cached.
location / {
proxy_pass http://127.0.0.1:8028;
}
}
What this does
Global cache storage
- proxy_cache_path
- /var/cache/nginx/odoo: where cached responses are stored on disk.
- levels=1:2: spreads files across nested folders for filesystem efficiency (1 char directory, then 2 chars).
- keys_zone=odoo_cache:10m: names the cache zone (odoo_cache) and allocates ~10 MB of in-memory index for cache keys/metadata (not the content).
- inactive=60m: if a cached item isn’t accessed for 60 minutes, NGINX can evict it even if its TTL hasn’t expired.
HTTPS server
- listen 443;
- Listening on HTTPS. (You’ll also need your SSL/TLS directives like ssl_certificate/ssl_certificate_key elsewhere in your server config; we’re focusing just on caching here.)
Cached API route: /api/contact
- location ~ ^/api/[a-z]+$
- Regex matches paths that start with /api/ followed by lowercase letters only (no numbers, no dashes, no extra segments).
- proxy_pass http://127.0.0.1:8028;
- Forwards matching requests to your Odoo backend at 127.0.0.1:8028.
- proxy_cache odoo_cache;
- Enables caching for this location using the odoo_cache zone defined above.
- proxy_cache_valid any 10m;
- All response codes (200, 404, etc.) will be cached for 10 minutes by default, unless the upstream sets its own cache headers. For example, in our controller we specified Cache-Control: max-age=60, so NGINX will cache it for 60 seconds. If no such header is provided, NGINX falls back to its 10-minute default.
- This is a simple, forceful policy — great for demos and read-only endpoints. For production, you might choose to be more specific (e.g., proxy_cache_valid 200 301 302 10m;).
- add_header X-Proxy-Cache $upstream_cache_status;
- Adds a helpful response header so you can see cache behavior in real time: MISS (first time), HIT (served from cache), BYPASS/EXPIRED/REVALIDATED (various states).
Everything else (not cached)
- location / { proxy_pass http://127.0.0.1:8028; }
- All other routes simply proxy to Odoo without caching. This avoids accidentally caching dynamic or user-specific pages.
How to test
Reload nginx
sudo nginx -t && sudo systemctl reload nginx
Call the API
curl -i https://your-domain.tld/api/contact
First call > X-Proxy-Cache: MISS
Repeat within 1 minute > X-Proxy-Cache: HIT
By enabling caching, you allow Nginx to serve repeated requests directly from memory or disk, reducing the load on your application server, cutting down response times, and ensuring smoother handling under traffic spikes.
To read more about Overview of Barcode and QR Scanner using mobile_scanner, refer to our blog Overview of Barcode and QR Scanner using mobile_scanner