March Docs

Cache

lib/cache.march — Cache

HTTP cache headers, response caching, and fragment caching for Bastion. All storage is backed by Vault (in-memory, per-node, TTL-aware).

── HTTP ETags ────────────────────────────────────────────────────────────────

Use Cache.etag_from/2 in handlers after the response body is known:

fn show(conn) do
  let html = render_page()
  conn
  |> Cache.cache_control("public, max-age=600")
  |> Cache.etag_from(html)   -- halts with 304 if client cache is fresh
  |> HttpServer.html(200, html)
end

── Response caching ──────────────────────────────────────────────────────────

Cache the full response for expensive routes:

fn index(conn) do
  Cache.cached(conn, "posts:index", 60, fn conn2 ->
    let posts = Blog.list_posts()
    HttpServer.html(conn2, 200, render_posts(posts))
  end)
end

── Fragment caching ──────────────────────────────────────────────────────────

Cache expensive rendered fragments:

fn sidebar_html() do
  Cache.fragment("sidebar", 300, fn () -> render_sidebar() end)
end

Functions

fncache_controlcache_control(conn, directives)#

Set the Cache-Control response header.

Common values:
  "public, max-age=3600"          — cache for 1 hour, shareable
  "private, max-age=0"            — client-only, no store
  "no-store"                      — never cache (e.g. sensitive data)
  "public, max-age=31536000"      — 1 year (use with content-hash URLs)

    conn |> Cache.cache_control("public, max-age=600")
fncachedcached(conn, key, ttl_secs, generator)#
fnetagetag(conn, tag_value)#

Set an explicit ETag value without computing it from content. Use this when you have a known version identifier (e.g. a record's updated_at).

    let tag = "v" ++ int_to_string(post.version)
    conn |> Cache.etag(tag)
fnetag_frometag_from(conn, content)#

Set an ETag from known content and short-circuit to 304 if the client already has a fresh copy.

  1. Computes SHA-256 of the content string and wraps it in quotes: "abc123".
  2. Sets the ETag response header.
  3. Reads If-None-Match from the request.
  4. If the ETags match, halts the conn with a 304 Not Modified response
   (empty body, no content-type). Subsequent calls like HttpServer.html are
   no-ops when the conn is halted.

Call this AFTER building the response body but BEFORE sending it:

    let html = render_page(data)
    conn
    |> Cache.etag_from(html)
    |> HttpServer.html(200, html)
fnfragmentfragment(key, ttl_secs, generator)#

Cache a rendered string fragment. On hit, returns the cached string. On miss, calls generator(), stores the result for ttl_secs, and returns it.

key      — cache key, e.g. "sidebar" or "user:42:avatar"
ttl_secs — how long to keep the fragment
generator — fn() -> String that renders the fragment

    let html = Cache.fragment("global_stats", 300, fn () -> render_stats() end)
fninvalidateinvalidate(key)#

Invalidate a cached response by key.

    Cache.invalidate("posts:index")
fninvalidate_fragmentinvalidate_fragment(key)#

Invalidate a cached fragment by key.

    Cache.invalidate_fragment("global_stats")
fninvalidate_prefixinvalidate_prefix(prefix)#

Invalidate all cached responses whose keys start with prefix. Useful for invalidating a group of related routes.

    Cache.invalidate_prefix("posts:")