HTTP Cache
This example shows how to cache dynamic GET responses with Vix middleware.
HTTP cache is useful when a route is safe, repeatable, and expensive enough that you do not want to run the handler every time.
Examples:
GET /api/products
GET /api/categories
GET /api/public-feed
GET /api/search?q=phone
GET /api/users?page=1This is not static file caching.
Static files are handled by:
app.static_dir(...);HTTP cache middleware is for dynamic route responses generated by handlers.
What this example builds
The app exposes:
GET /api/health
GET /api/products
GET /api/slow-products
GET /api/products-by-languageThe cache is installed on:
/apiOnly GET responses are cached.
The example demonstrates:
cache miss
cache hit
cache bypass
query-aware cache keys
Vary headers
slow handler skipped on cache hitSource
Create a file:
http_cache_demo.cppAdd this code:
#include <chrono>
#include <string>
#include <thread>
#include <vector>
#include <vix.hpp>
#include <vix/middleware.hpp>
#include <vix/json.hpp>
using namespace vix;
struct Product
{
int id;
std::string name;
double price;
};
static std::vector<Product> products{
{1, "Laptop", 999.99},
{2, "Phone", 499.50},
{3, "Tablet", 299.99}
};
static vix::json::Json product_to_json(const Product &product)
{
using namespace vix::json;
return o(
"id", product.id,
"name", product.name,
"price", product.price
);
}
static vix::json::Json products_to_json()
{
using namespace vix::json;
Json items = arr();
for (const auto &product : products)
{
items.push_back(product_to_json(product));
}
return items;
}
static void install_middleware(App &app)
{
app.use("/api", middleware::app::request_id_dev());
app.use("/api", middleware::app::timing_dev());
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass",
.vary_headers = {"accept-language"}
}));
}
static void register_routes(App &app)
{
app.get("/", [](Request &, Response &res)
{
res.text("HTTP cache example. Try /api/products or /api/slow-products.");
});
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true,
"service", "http-cache"
});
});
app.get("/api/products", [](Request &req, Response &res)
{
using namespace vix::json;
const std::string page = req.query_value("page", "1");
res.json(o(
"ok", true,
"source", "origin",
"page", page,
"products", products_to_json()
));
});
app.get("/api/slow-products", [](Request &, Response &res)
{
using namespace vix::json;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
res.json(o(
"ok", true,
"source", "origin",
"delay_ms", 500,
"products", products_to_json()
));
});
app.get("/api/products-by-language", [](Request &req, Response &res)
{
using namespace vix::json;
const std::string language =
req.has_header("accept-language")
? req.header("accept-language")
: "none";
res.json(o(
"ok", true,
"source", "origin",
"accept_language", language,
"products", products_to_json()
));
});
}
int main()
{
App app;
install_middleware(app);
register_routes(app);
app.run(8080);
return 0;
}Run it
vix run http_cache_demo.cppThe server listens on:
http://127.0.0.1:8080Test the public route
curl -i http://127.0.0.1:8080/Expected body:
HTTP cache example. Try /api/products or /api/slow-products.This route is not under /api, so the HTTP cache middleware does not apply to it.
Test the first cache miss
curl -i http://127.0.0.1:8080/api/productsExpected cache status:
x-vix-cache-status: missExpected body shape:
{
"ok": true,
"source": "origin",
"page": "1",
"products": [
{
"id": 1,
"name": "Laptop",
"price": 999.99
}
]
}A miss means:
the cache did not have a valid response
the route handler ran
the response was storedTest the cache hit
Call the same route again:
curl -i http://127.0.0.1:8080/api/productsExpected cache status:
x-vix-cache-status: hitA hit means:
the cached response was replayed
the route handler was skippedThis is the main value of HTTP cache middleware.
Test cache bypass
Send the bypass header:
curl -i \
http://127.0.0.1:8080/api/products \
-H "x-vix-cache: bypass"Expected cache status:
x-vix-cache-status: bypassA bypass means:
the request asked the middleware to skip the cache
the route handler ranThis is useful for debugging and manual refresh.
Test query-aware cache keys
The cache key includes the query string.
First request:
curl -i "http://127.0.0.1:8080/api/products?page=1"Second request:
curl -i "http://127.0.0.1:8080/api/products?page=1"Expected second response:
x-vix-cache-status: hitDifferent page:
curl -i "http://127.0.0.1:8080/api/products?page=2"Expected behavior:
different query string
different cache key
new cache missThe response for page=1 and page=2 should not be shared.
Test the slow route
The route /api/slow-products sleeps for 500 ms before responding.
First request:
time curl -i http://127.0.0.1:8080/api/slow-productsExpected cache status:
x-vix-cache-status: missSecond request:
time curl -i http://127.0.0.1:8080/api/slow-productsExpected cache status:
x-vix-cache-status: hitThe second request should be faster because the handler is skipped.
Bypass the slow route:
time curl -i \
http://127.0.0.1:8080/api/slow-products \
-H "x-vix-cache: bypass"Expected cache status:
x-vix-cache-status: bypassThe bypassed request runs the slow handler again.
Test Vary headers
The middleware is configured with:
.vary_headers = {"accept-language"}This means the Accept-Language header becomes part of the cache key.
French request:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: fr"Call it again:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: fr"Expected second response:
x-vix-cache-status: hitEnglish request:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: en"Expected behavior:
different Accept-Language
different cache key
new cache missUse vary_headers when the same path can return different content depending on request headers.
Why Vary matters
Without vary_headers, this route could be wrong:
app.get("/api/products-by-language", [](Request &req, Response &res)
{
const std::string language =
req.has_header("accept-language")
? req.header("accept-language")
: "none";
res.json({
"accept_language", language
});
});If the first cached response was French, an English request could receive the French response.
vary_headers prevents that by separating cache entries.
Good vary headers:
accept-language
accept
x-tenant-id
x-currencyBad vary headers:
headers that do not change the response
high-cardinality headers with no clear value
random request ids
user agents unless truly neededA vary header should only be used if it changes the response.
HTTP cache configuration
The example uses:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass",
.vary_headers = {"accept-language"}
}));Meaning:
ttl_ms = 30 seconds
allow_bypass = clients can force origin
bypass_header = x-vix-cache
bypass_value = bypass
vary_headers = accept-language participates in the cache keyUse a custom cache instance
You can share a cache instance explicitly.
auto cache = middleware::app::make_default_cache({
.ttl_ms = 30'000
});
app.use("/api", middleware::app::http_cache_mw({
.prefix = "/api",
.only_get = true,
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass",
.vary_headers = {"accept-language"},
.cache = cache,
.add_debug_header = true,
.debug_header = "x-vix-cache-status"
}));Use this when:
multiple middleware instances should share one cache
you want to build the cache object yourself
you want a clearer bootstrap structureFor most examples, middleware::app::http_cache(...) is enough.
Where to install HTTP cache
Install HTTP cache on route groups that contain safe GET routes.
Good:
app.use("/api/products", middleware::app::http_cache({
.ttl_ms = 30'000
}));Also good:
app.use("/api/public", middleware::app::http_cache({
.ttl_ms = 30'000
}));Be careful with:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000
}));This is fine when the middleware only applies to GET, but you should still think about which GET routes under /api are safe to cache.
What should be cached
Good candidates:
public product lists
public categories
public blog posts
public search results
public metadata
slow read-only endpointsBad candidates:
current user profile
cart
orders
notifications
private messages
admin dashboard
routes that depend on cookies or hidden session stateThe safe rule is:
cache public GET routes first
avoid private user-specific routes until the cache key is designed carefullyAuthenticated routes
Be careful with authenticated routes.
If the response depends on the user, the cache key must separate users.
Possible vary inputs:
authorization
x-user-id
x-tenant-idBut those can create many cache entries and can be dangerous if misused.
The recommended starting point is:
do not cache private user-specific routes
cache public GET routes firstHTTP cache vs static file cache
Static file cache:
app.static_dir(
"public",
"/",
"index.html",
true,
"public, max-age=3600",
true,
true
);Dynamic HTTP cache:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000
}));The difference:
static file cache
browser/proxy cache policy for files served from public/
dynamic HTTP cache
server-side response cache for route handlersDo not use http_cache to explain static files.
Do not use app.static_dir(...) to explain dynamic response caching.
They are different parts of Vix.
HTTP cache vs ETag
HTTP cache and ETag solve different problems.
HTTP cache
server stores response
cache hit can skip handler
ETag
response gets a validation tag
client can revalidate
server may return 304Use HTTP cache when the server should avoid repeated work.
Use ETag when the client should avoid downloading the same body again.
They can be combined, but start with one feature and test the headers.
Common response headers
When HTTP cache is active, you may see:
x-vix-cache-status: miss
x-vix-cache-status: hit
x-vix-cache-status: bypassWhen timing middleware is active, you may see:
x-response-time: ...
server-timing: total;dur=...When request id middleware is active, you may see:
x-request-id: ...These headers make the example easier to test.
Complete test flow
Run:
vix run http_cache_demo.cppFirst products request:
curl -i http://127.0.0.1:8080/api/productsSecond products request:
curl -i http://127.0.0.1:8080/api/productsBypass:
curl -i \
http://127.0.0.1:8080/api/products \
-H "x-vix-cache: bypass"Query key:
curl -i "http://127.0.0.1:8080/api/products?page=1"
curl -i "http://127.0.0.1:8080/api/products?page=2"Slow route:
time curl -i http://127.0.0.1:8080/api/slow-products
time curl -i http://127.0.0.1:8080/api/slow-productsVary header:
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: fr"
curl -i \
http://127.0.0.1:8080/api/products-by-language \
-H "Accept-Language: en"Summary
Use HTTP cache middleware for dynamic GET routes that are safe to replay.
Start with:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass"
}));Remember:
first request
miss, handler runs
same request again
hit, handler skipped
bypass header
bypass, handler runs
different query string
different cache key
vary header
separates responses that depend on request headersKeep the separation clear:
HTTP cache middleware
dynamic GET response cache
app.static_dir(...)
static file serving and static cache headers