HTTP Cache
The HTTP cache middleware stores and replays dynamic GET responses.
It is useful when a route is expensive but returns the same response for the same request key during a short time window.
Examples:
GET /api/users
GET /api/products?page=1
GET /api/categories
GET /api/public-feed
GET /api/search?q=phone2
3
4
5
The HTTP cache middleware is different from static file caching.
app.static_dir(...)
serves public files and can add Cache-Control headers
middleware::app::http_cache(...)
caches dynamic GET responses produced by route handlers2
3
4
5
This page is about dynamic route responses.
What it does
The HTTP cache middleware sits before your route handler.
On a cache miss:
request
-> HTTP cache middleware
-> route handler runs
-> response is stored
-> response is sent2
3
4
5
On a cache hit:
request
-> HTTP cache middleware
-> cached response is replayed
-> route handler is not called2
3
4
That is the main value.
Expensive handlers can be skipped when the response is already cached.
Basic example
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass"
}));
app.get("/api/users", [](Request &, Response &res)
{
res.json({
"ok", true,
"source", "origin"
});
});
app.get("/", [](Request &, Response &res)
{
res.text("home route is not cached");
});
app.run(8080);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Run:
vix run http_cache_demo.cppFirst request:
curl -i http://127.0.0.1:8080/api/usersExpected cache status:
x-vix-cache-status: missSecond request:
curl -i http://127.0.0.1:8080/api/usersExpected cache status:
x-vix-cache-status: hitBypass the cache:
curl -i \
http://127.0.0.1:8080/api/users \
-H "x-vix-cache: bypass"2
3
Expected cache status:
x-vix-cache-status: bypassUse it only for safe GET routes
The HTTP cache middleware is designed for GET responses.
Do not use it for routes that mutate state.
Good candidates:
GET /api/products
GET /api/categories
GET /api/posts
GET /api/public-profile
GET /api/search2
3
4
5
Bad candidates:
POST /api/orders
PUT /api/products/1
PATCH /api/profile
DELETE /api/items/1
GET routes that depend on hidden user/session state2
3
4
5
A cache hit skips the route handler.
That is excellent for safe public data.
It is dangerous if the handler must always run.
Cache key
The middleware builds a cache key from request data.
The key can include:
HTTP method
path
query string
selected headers2
3
4
This means different query strings can produce different cached responses.
Example:
/api/products?page=1
/api/products?page=22
These should not share the same cached response.
Header-based variants can also be supported with vary_headers.
Vary headers
Use vary_headers when a response changes depending on request headers.
Example:
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"}
}));2
3
4
5
6
7
Route:
app.get("/api/users", [](Request &req, Response &res)
{
const std::string lang =
req.has_header("accept-language")
? req.header("accept-language")
: "none";
res.json({
"ok", true,
"source", "origin",
"accept_language", lang
});
});2
3
4
5
6
7
8
9
10
11
12
13
Request in French:
curl -i \
http://127.0.0.1:8080/api/users \
-H "Accept-Language: fr"2
3
Request in English:
curl -i \
http://127.0.0.1:8080/api/users \
-H "Accept-Language: en"2
3
These can produce separate cache entries.
Use vary_headers when the route response depends on headers such as:
accept-language
accept
x-tenant-id
x-currency2
3
4
Do not vary on headers that do not affect the response.
Unnecessary vary headers reduce cache reuse.
Bypass header
The bypass header lets a client force the origin handler to run.
Default-style configuration:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass"
}));2
3
4
5
6
Bypass request:
curl -i \
http://127.0.0.1:8080/api/users \
-H "x-vix-cache: bypass"2
3
This is useful for:
debugging
manual refresh
admin tools
checking origin response2
3
4
Disable bypass if clients should never be able to skip cache.
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = false
}));2
3
4
Cache status header
The middleware can write a cache status header.
Common values are:
miss
hit
bypass2
3
Example:
x-vix-cache-status: missA miss means the handler ran and the response may have been stored.
A hit means the response came from cache and the handler was skipped.
A bypass means the request asked to skip cache.
Use this header during development and diagnostics.
TTL
ttl_ms controls how long a cached response can be reused.
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000
}));2
3
This stores responses for about 30 seconds.
Use short TTLs for data that changes often.
Use longer TTLs for public data that changes rarely.
Examples:
| Data | Suggested TTL |
|---|---|
| Health metadata | Very short or no cache |
| Public categories | Longer |
| Product lists | Short to medium |
| Search results | Short |
| User-specific data | Avoid caching unless keyed safely |
The cache does not replace database correctness.
It only reduces repeated handler work for safe responses.
Custom cache instance
You can inject a custom cache instance.
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
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 = {},
.cache = cache,
.add_debug_header = true,
.debug_header = "x-vix-cache-status"
}));
app.get("/api/slow", [](Request &, Response &res)
{
res.text("slow response from origin");
});
app.run(8080);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Use a custom cache when:
several middleware instances should share one cache
you want to control the cache policy explicitly
you want to inject a different store later2
3
Slow route example
This example makes cache behavior visible.
#include <chrono>
#include <thread>
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass"
}));
app.get("/api/slow", [](Request &, Response &res)
{
std::this_thread::sleep_for(std::chrono::milliseconds(250));
res.text("slow response from origin");
});
app.run(8080);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
First request:
time curl -i http://127.0.0.1:8080/api/slowSecond request:
time curl -i http://127.0.0.1:8080/api/slowThe second request should avoid the artificial delay if it is served from cache.
Bypass:
time curl -i \
http://127.0.0.1:8080/api/slow \
-H "x-vix-cache: bypass"2
3
The bypassed request should run the handler again.
Configuration options
App-level HTTP cache configuration:
middleware::app::HttpCacheConfig{
.prefix = "/api/",
.only_get = true,
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass",
.vary_headers = {},
.cache = nullptr,
.add_debug_header = false,
.debug_header = "x-vix-cache-status"
}2
3
4
5
6
7
8
9
10
11
12
Main options:
| Option | Purpose |
|---|---|
prefix | Prefix used by install helpers |
only_get | Apply cache only to GET requests |
ttl_ms | Cache lifetime in milliseconds |
allow_bypass | Allow bypass header |
bypass_header | Header used to request bypass |
bypass_value | Header value that triggers bypass |
vary_headers | Headers included in the cache key |
cache | Custom cache instance |
add_debug_header | Add cache status header for diagnostics |
debug_header | Name of cache status header |
For most applications, start with:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000
}));2
3
Then add bypass and vary headers as needed.
Lower-level HTTP cache middleware
The lower-level middleware is:
vix::middleware::http_cache(...)It returns:
vix::middleware::HttpMiddlewareThe App helper adapts it for vix::App.
Use the lower-level API when building custom integrations.
Normal applications should use:
middleware::app::http_cache(...)or:
middleware::app::http_cache_mw(...)Cache and status codes
By default, the middleware should cache successful 200 responses.
Avoid caching error responses unless you explicitly want that behavior.
A useful rule:
cache 200 OK
avoid caching 401, 403, 404, 422, 5002
This prevents temporary failures from being replayed longer than intended.
Cache and response bodies
Some responses should not be cached.
Avoid caching:
empty responses when the body matters
streaming responses
user-specific private responses
responses with volatile data
responses depending on cookies or sessions2
3
4
5
Good cached responses are usually:
public
safe
repeatable
not tied to one user session
valid for a short TTL2
3
4
5
Cache and authenticated routes
Be careful caching authenticated routes.
If a response depends on the authenticated user, the cache key must include something that separates users.
Examples:
Authorization
x-user-id
x-tenant-id2
3
But caching by Authorization can create many cache entries and may be undesirable.
The safest starting rule is:
cache public GET routes first
avoid private user-specific routes until the cache key is designed carefully2
Good first targets:
public categories
public product lists
public blog posts
public docs metadata2
3
4
Risky first targets:
current user profile
admin dashboard
cart
orders
notifications
private messages2
3
4
5
6
HTTP cache vs ETag
HTTP cache middleware stores the response on the server side and can skip the handler on cache hits.
ETag helps the client revalidate a response.
HTTP cache
server-side response reuse
handler can be skipped
ETag
client-side validation
client may receive 304 Not Modified2
3
4
5
6
7
They can be used together, but they solve different problems.
Use HTTP cache when the server should avoid repeated handler work.
Use ETag when clients should avoid downloading the same body again.
HTTP cache vs static files
Static files are served by Core through:
app.static_dir(...)Static files can receive Cache-Control headers from Core configuration.
HTTP cache middleware is for dynamic route responses:
app.get("/api/users", [](Request &, Response &res)
{
res.json({
"ok", true
});
});2
3
4
5
6
Keep this separation:
Core static files
public files, index.html, SPA fallback, Cache-Control
HTTP cache middleware
dynamic GET route responses2
3
4
5
Do not document static file serving as HTTP cache middleware.
Complete example
#include <chrono>
#include <thread>
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::rate_limit_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"}
}));
app.get("/api/users", [](Request &req, Response &res)
{
std::this_thread::sleep_for(std::chrono::milliseconds(250));
const std::string lang =
req.has_header("accept-language")
? req.header("accept-language")
: "none";
res.json({
"ok", true,
"source", "origin",
"accept_language", lang
});
});
app.get("/", [](Request &, Response &res)
{
res.text("public home, not cached by /api middleware");
});
app.run(8080);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Run:
vix run http_cache_complete.cppFirst request:
curl -i http://127.0.0.1:8080/api/usersSecond request:
curl -i http://127.0.0.1:8080/api/usersDifferent language:
curl -i \
http://127.0.0.1:8080/api/users \
-H "Accept-Language: fr"2
3
Bypass:
curl -i \
http://127.0.0.1:8080/api/users \
-H "x-vix-cache: bypass"2
3
Expected behavior:
first request
cache miss, handler runs
second same request
cache hit, handler skipped
different Accept-Language
different cache key
bypass
handler runs again2
3
4
5
6
7
8
9
10
11
Summary
Use HTTP cache middleware for dynamic GET routes that are safe to replay for a short time.
Good starting point:
app.use("/api", middleware::app::http_cache({
.ttl_ms = 30'000,
.allow_bypass = true,
.bypass_header = "x-vix-cache",
.bypass_value = "bypass"
}));2
3
4
5
6
Remember:
cache public GET routes first
use vary_headers when headers change the response
avoid private user-specific routes until the cache key is designed carefully
static files belong to Core, not HTTP cache middleware2
3
4