Vix.cpp v2.6.0 is here Read the blog
Skip to content

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:

txt
GET /api/products
GET /api/categories
GET /api/public-feed
GET /api/search?q=phone
GET /api/users?page=1
1
2
3
4
5

This is not static file caching.

Static files are handled by:

cpp
app.static_dir(...);
1

HTTP cache middleware is for dynamic route responses generated by handlers.

What this example builds

The app exposes:

txt
GET /api/health
GET /api/products
GET /api/slow-products
GET /api/products-by-language
1
2
3
4

The cache is installed on:

txt
/api
1

Only GET responses are cached.

The example demonstrates:

txt
cache miss
cache hit
cache bypass
query-aware cache keys
Vary headers
slow handler skipped on cache hit
1
2
3
4
5
6

Source

Create a file:

txt
http_cache_demo.cpp
1

Add this code:

cpp
#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;
}
1
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

Run it

bash
vix run http_cache_demo.cpp
1

The server listens on:

txt
http://127.0.0.1:8080
1

Test the public route

bash
curl -i http://127.0.0.1:8080/
1

Expected body:

txt
HTTP cache example. Try /api/products or /api/slow-products.
1

This route is not under /api, so the HTTP cache middleware does not apply to it.

Test the first cache miss

bash
curl -i http://127.0.0.1:8080/api/products
1

Expected cache status:

txt
x-vix-cache-status: miss
1

Expected body shape:

json
{
  "ok": true,
  "source": "origin",
  "page": "1",
  "products": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 999.99
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12

A miss means:

txt
the cache did not have a valid response
the route handler ran
the response was stored
1
2
3

Test the cache hit

Call the same route again:

bash
curl -i http://127.0.0.1:8080/api/products
1

Expected cache status:

txt
x-vix-cache-status: hit
1

A hit means:

txt
the cached response was replayed
the route handler was skipped
1
2

This is the main value of HTTP cache middleware.

Test cache bypass

Send the bypass header:

bash
curl -i \
  http://127.0.0.1:8080/api/products \
  -H "x-vix-cache: bypass"
1
2
3

Expected cache status:

txt
x-vix-cache-status: bypass
1

A bypass means:

txt
the request asked the middleware to skip the cache
the route handler ran
1
2

This is useful for debugging and manual refresh.

Test query-aware cache keys

The cache key includes the query string.

First request:

bash
curl -i "http://127.0.0.1:8080/api/products?page=1"
1

Second request:

bash
curl -i "http://127.0.0.1:8080/api/products?page=1"
1

Expected second response:

txt
x-vix-cache-status: hit
1

Different page:

bash
curl -i "http://127.0.0.1:8080/api/products?page=2"
1

Expected behavior:

txt
different query string
different cache key
new cache miss
1
2
3

The 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:

bash
time curl -i http://127.0.0.1:8080/api/slow-products
1

Expected cache status:

txt
x-vix-cache-status: miss
1

Second request:

bash
time curl -i http://127.0.0.1:8080/api/slow-products
1

Expected cache status:

txt
x-vix-cache-status: hit
1

The second request should be faster because the handler is skipped.

Bypass the slow route:

bash
time curl -i \
  http://127.0.0.1:8080/api/slow-products \
  -H "x-vix-cache: bypass"
1
2
3

Expected cache status:

txt
x-vix-cache-status: bypass
1

The bypassed request runs the slow handler again.

Test Vary headers

The middleware is configured with:

cpp
.vary_headers = {"accept-language"}
1

This means the Accept-Language header becomes part of the cache key.

French request:

bash
curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: fr"
1
2
3

Call it again:

bash
curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: fr"
1
2
3

Expected second response:

txt
x-vix-cache-status: hit
1

English request:

bash
curl -i \
  http://127.0.0.1:8080/api/products-by-language \
  -H "Accept-Language: en"
1
2
3

Expected behavior:

txt
different Accept-Language
different cache key
new cache miss
1
2
3

Use 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:

cpp
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
  });
});
1
2
3
4
5
6
7
8
9
10
11

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:

txt
accept-language
accept
x-tenant-id
x-currency
1
2
3
4

Bad vary headers:

txt
headers that do not change the response
high-cardinality headers with no clear value
random request ids
user agents unless truly needed
1
2
3
4

A vary header should only be used if it changes the response.

HTTP cache configuration

The example uses:

cpp
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"}
}));
1
2
3
4
5
6
7

Meaning:

txt
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 key
1
2
3
4
5

Use a custom cache instance

You can share a cache instance explicitly.

cpp
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"
}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Use this when:

txt
multiple middleware instances should share one cache
you want to build the cache object yourself
you want a clearer bootstrap structure
1
2
3

For 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:

cpp
app.use("/api/products", middleware::app::http_cache({
  .ttl_ms = 30'000
}));
1
2
3

Also good:

cpp
app.use("/api/public", middleware::app::http_cache({
  .ttl_ms = 30'000
}));
1
2
3

Be careful with:

cpp
app.use("/api", middleware::app::http_cache({
  .ttl_ms = 30'000
}));
1
2
3

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:

txt
public product lists
public categories
public blog posts
public search results
public metadata
slow read-only endpoints
1
2
3
4
5
6

Bad candidates:

txt
current user profile
cart
orders
notifications
private messages
admin dashboard
routes that depend on cookies or hidden session state
1
2
3
4
5
6
7

The safe rule is:

txt
cache public GET routes first
avoid private user-specific routes until the cache key is designed carefully
1
2

Authenticated routes

Be careful with authenticated routes.

If the response depends on the user, the cache key must separate users.

Possible vary inputs:

txt
authorization
x-user-id
x-tenant-id
1
2
3

But those can create many cache entries and can be dangerous if misused.

The recommended starting point is:

txt
do not cache private user-specific routes
cache public GET routes first
1
2

HTTP cache vs static file cache

Static file cache:

cpp
app.static_dir(
  "public",
  "/",
  "index.html",
  true,
  "public, max-age=3600",
  true,
  true
);
1
2
3
4
5
6
7
8
9

Dynamic HTTP cache:

cpp
app.use("/api", middleware::app::http_cache({
  .ttl_ms = 30'000
}));
1
2
3

The difference:

txt
static file cache
  browser/proxy cache policy for files served from public/

dynamic HTTP cache
  server-side response cache for route handlers
1
2
3
4
5

Do 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.

txt
HTTP cache
  server stores response
  cache hit can skip handler

ETag
  response gets a validation tag
  client can revalidate
  server may return 304
1
2
3
4
5
6
7
8

Use 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:

txt
x-vix-cache-status: miss
x-vix-cache-status: hit
x-vix-cache-status: bypass
1
2
3

When timing middleware is active, you may see:

txt
x-response-time: ...
server-timing: total;dur=...
1
2

When request id middleware is active, you may see:

txt
x-request-id: ...
1

These headers make the example easier to test.

Complete test flow

Run:

bash
vix run http_cache_demo.cpp
1

First products request:

bash
curl -i http://127.0.0.1:8080/api/products
1

Second products request:

bash
curl -i http://127.0.0.1:8080/api/products
1

Bypass:

bash
curl -i \
  http://127.0.0.1:8080/api/products \
  -H "x-vix-cache: bypass"
1
2
3

Query key:

bash
curl -i "http://127.0.0.1:8080/api/products?page=1"
curl -i "http://127.0.0.1:8080/api/products?page=2"
1
2

Slow route:

bash
time curl -i http://127.0.0.1:8080/api/slow-products
time curl -i http://127.0.0.1:8080/api/slow-products
1
2

Vary header:

bash
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"
1
2
3
4
5
6
7

Summary

Use HTTP cache middleware for dynamic GET routes that are safe to replay.

Start with:

cpp
app.use("/api", middleware::app::http_cache({
  .ttl_ms = 30'000,
  .allow_bypass = true,
  .bypass_header = "x-vix-cache",
  .bypass_value = "bypass"
}));
1
2
3
4
5
6

Remember:

txt
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 headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Keep the separation clear:

txt
HTTP cache middleware
  dynamic GET response cache

app.static_dir(...)
  static file serving and static cache headers
1
2
3
4
5

Released under the MIT License.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.