Skip to content
IndraGunawanPublic

About

High-throughput, low-allocation RFC-compliant HTTP caching middleware for Go and API gateways.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

65 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Titip

Go Reference Go Version

Titip is a high-throughput, low-allocation HTTP caching middleware for Go applications and API gateways.

Designed for high-concurrency services, Titip reduces backend load by serving cached responses with minimal memory allocation, atomic multi-variant negotiation, RFC-compliant freshness calculations, and fail-open resilience.

Key Features

  • Low-Allocation Design: Reuses internal memory buffers and decompression streams to minimize heap allocations under high concurrency.
  • Storage Compression: Built-in LZ4 (default) and Zstandard (zstd) compression for stored variant bodies to reduce memory footprint and storage I/O, with pluggable custom codec support.
  • Fail-Open Resilience: Storage outages, decompression errors, or upstream panics safely bypass to the origin handler (fwd=bypass) or serve stale cache without terminating the process.
  • Strict Privacy & Explicit Opt-In: Titip never caches heuristically. Responses must explicitly declare a freshness lifetime (max-age, s-maxage, or Expires). Cold URL misses execute independently without singleflight coalescing, and responses with Set-Cookie or private directives bypass caching unconditionally.
  • RFC-7234, RFC-9111 & RFC-9213 Compliant: Implements the official Age & Freshness calculation standard (apparent age, corrected initial age, resident time, clock-skew correction, and multi-variant Vary header negotiation).
  • Tiered Cache-Control (RFC 9213): Supports targeted header resolution (Titip-Cache-Control → CDN-Cache-Control → Cache-Control), allowing backends to configure edge caching independently from browser caching.
  • RFC-9211 Cache-Status Observability: Structured diagnostics (Cache-Status: titip; hit; ttl=295, fwd=stale, fwd=bypass) with multi-tier cache chaining.
  • Edge Side Includes (ESI): Concurrent fragment assembly, in-process routing, recursive loop protection, and SSRF prevention.
  • Granular Cache Purge API: Invalidation via programmatic Go API (exact URL, wildcard prefixes, surrogate Cache-Tag, soft-purge, or full cache purge).
  • Pluggable Architecture: Standard net/http middleware with modular framework adapters and decoupled storage engines.

Architecture

Titip separates metadata from variant payloads to enable atomic multi-variant negotiation (Vary) and serve conditional responses (304 Not Modified) without reading the response body from storage:

[ Incoming Request ] ──► (Primary Key: Scheme + Host + Path + Filtered Query)
                               │
                               ▼
        ┌──────────────────────────────────────────────┐
        │ Stage 1: GetMeta(primaryKey)                 │
        │ Evaluates: Vary headers, freshness, tags     │
        └──────────────────────┬───────────────────────┘
                               │
        ┌──────────────────────┴───────────────────────┐
        │ Match Vary Variant & Evaluate Freshness      │
        └──────┬───────────────────────────────┬───────┘
               │                               │
      (Downstream 304 / HEAD)             (Cache Hit)
               │                               │
               ▼                               ▼
    ┌──────────────────────┐       ┌──────────────────────────────┐
    │ Serve 304 / Headers  │       │ Stage 2: GetVariant(pk, vk)  │
    │ (No Body Read Needed)│       │ Returns: Headers & Body      │
    └──────────────────────┘       └──────────────┬───────────────┘
                                                  │
                                                  ▼
                                   ┌──────────────────────────────┐
                                   │Decompress & Stream (LZ4/Zstd)│
                                   └──────────────────────────────┘

Modules & Ecosystem

Titip is organized as a multi-module workspace. Each module is versioned independently:

Module Description Documentation
github.com/indragunawan/titip Core caching middleware, state machine, and programmatic Purge API Core Quickstart
github.com/indragunawan/titip/adapter/caddy Native Caddy HTTP middleware directive (titip) & Admin Purge API Caddy Adapter Guide
github.com/indragunawan/titip/storage/redis High-performance Redis (7.4+), Valkey (9.0+), DragonflyDB (1.38+) driver (rueidis) Redis Storage Guide
github.com/indragunawan/titip/storage/redis/caddy Guest storage module for Caddy (titip.storage.redis) Caddy Redis Guide

Quickstart

Install the core package and Redis storage driver:

go get github.com/indragunawan/titip
go get github.com/indragunawan/titip/storage/redis

Wrap any standard net/http handler:

package main

import (
    "context"
    "net/http"
    "time"

    "github.com/redis/rueidis"

    "github.com/indragunawan/titip"
    storageRedis "github.com/indragunawan/titip/storage/redis"
)

func main() {
    // Initialize Redis client
    client, err := rueidis.NewClient(rueidis.ClientOption{
        InitAddress: []string{"127.0.0.1:6379"},
    })
    if err != nil {
        panic(err)
    }
    defer client.Close()

    // Create Titip Redis storage
    store, err := storageRedis.New(client, storageRedis.WithKeyPrefix("titip:"))
    if err != nil {
        panic(err)
    }

    // Configure Titip engine
    cache, err := titip.New(
        store,
        titip.WithCacheStatus(titip.CacheStatusRFC9211),
    )
    if err != nil {
        panic(err)
    }
    defer cache.Close(context.Background())

    // Wrap standard HTTP handler
    origin := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        // Cache publicly for 60 seconds, allow serving stale for 5 minutes during revalidation
        w.Header().Set("Cache-Control", "public, max-age=60, stale-while-revalidate=300")
        w.Header().Set("Cache-Tag", "catalog items")
        w.Write([]byte(`{"message": "hello from origin", "timestamp": "` + time.Now().Format(time.RFC3339) + `"}`))
    })

    http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        cache.ServeHTTP(w, r, origin)
    }))
}

Configuration Reference

Initialize Titip with the mandatory storage engine and any functional options via titip.New(store storage.Storage, opts ...Option):

Option Type Default Description
WithCacheStatus(mode) CacheStatusMode CacheStatusSimple Emitted status format (CacheStatusRFC9211, CacheStatusSimple, or CacheStatusNone).
WithCacheKey(cfg) CacheKey {} (standard) Primary cache key generation rules and query parameter filtering.
WithTagHeader(name) string "Cache-Tag" Response header inspected for surrogate cache tags.
WithBackgroundFetchTimeout(d) time.Duration 125s Maximum timeout budget for background revalidations (stale-while-revalidate).
WithStorageTimeout(d) time.Duration 5s Maximum time budget for storage reads/writes before fail-open bypass.
WithRespectClientCacheControl() - (disabled) When enabled, honors client request Cache-Control: no-cache / no-store.
WithoutConvertHeadToGet() - (enabled) Disables converting origin HEAD cache misses to GET to prime the cache with body bytes.
WithAutoInvalidateMutatingMethods() - (disabled) RFC 9111 §4.4: Auto-purges URI cache when mutating requests (POST/PUT/DELETE) succeed.
WithLogger(l) *slog.Logger slog.Default() Structured logger instance for diagnostic events.
WithMetrics(reg) prometheus.Registerer nil Prometheus registry for cache and ESI telemetry.
WithESI(opts...) ...esi.Option disabled Edge Side Includes processing configuration and options.
WithServerTiming() - (disabled) Enables Server-Timing header diagnostics for TTFB tracing in browser DevTools.
WithServerTimingCookie(name, val) string, string "" Restricts Server-Timing header generation to requests matching an exact cookie name and value.
WithStorageCompression(name) string "lz4" Selected compression codec for stored variant bodies ("lz4", "zstd", or "none").
WithStorageCompressor(comp) Compressor - Registers a custom compression engine implementing Compressor for storage reads/writes.

Cache Key & Query Parameter Normalization

Titip constructs normalized cache keys directly without expensive hashing. Use CacheKey to filter query parameters and strip tracking tags to prevent cache fragmentation:

cache, err := titip.New(
    store,
    titip.WithCacheKey(titip.CacheKey{
        // Strip marketing query parameters (utm_*, fbclid, gclid, mc_eid, etc.)
        ExcludeMarketingQueryParams: true,
        // Allowlist specific query parameters to include (or use ExcludedQueryParams for a denylist)
        IncludedQueryParams: []string{"page", "sort", "filter"},
    }),
)

CacheKey Configuration Reference

Field Type Default Description
IncludeProtocol bool false Includes scheme (http:// vs https://) in the cache key.
ExcludeHost bool false Excludes hostname from the cache key so multiple domains share cache entries.
ExcludeQuery bool false Ignores all query parameters entirely.
PreserveQueryOrder bool false Preserves URL query parameter order instead of sorting alphabetically.
IncludedQueryParams []string (all) Allowlist of query parameters to keep in the cache key.
ExcludedQueryParams []string (none) Denylist of query parameters to exclude from the cache key.
ExcludeMarketingQueryParams bool false Strips tracking tags (utm_*, fbclid, gclid, ttclid, etc.).
IncludedQueryParamValues map[string][]string (none) Allowlists specific accepted values per query parameter.
IncludedHeaderNames []string (none) Appends specific request header values to the primary key (e.g. X-Region).
IncludedCookieNames []string (none) Appends specific cookie values to the primary key (e.g. currency, theme).
CaseInsensitivePath bool false Lowercases the URL path for case-insensitive matching.

Cache-Status Diagnostics

Titip supports three Cache-Status modes configured via WithCacheStatus:

1. CacheStatusSimple (Simple Status Header) -- default

Emits a concise status header:

Status Description
HIT Served fresh directly from cache or matched downstream conditional 304 Not Modified.
MISS Cache miss: fetched from origin and stored in cache.
EXPIRED Expired cache entry was synchronously revalidated with origin and refreshed (200 OK).
REVALIDATED Expired cache entry was revalidated with origin via conditional headers (304 Not Modified).
UPDATING Stale cache entry served immediately while revalidating asynchronously in the background (stale-while-revalidate).
STALE Stale cache entry served as failover fallback due to origin error (stale-if-error).
BYPASS Caching explicitly bypassed (mutating method, client no-store, Range request, WebSocket).
DYNAMIC Evaluated for caching, but origin response is uncacheable (missing explicit max-age/s-maxage/Expires, or contains Set-Cookie, private, no-store).

2. CacheStatusRFC9211 (Structured Header)

Emits structured diagnostics compliant with RFC 9211, supporting multi-tier cache chaining:

Cache-Status: titip; hit; ttl=240

3. CacheStatusNone

Disables the Cache-Status response header completely.

Server-Timing Diagnostics

Titip supports the standard Server-Timing header to inspect cache lookup, decompression, and origin latency in browser DevTools (Network Timing):

# Cache Hit (with LZ4 decompression)
Server-Timing: titip-status;desc="HIT", titip-meta;dur=1.49, titip-body;dur=3.39;desc="lz4", titip;dur=4.95

# Cache Miss (storing compressed variant)
Server-Timing: titip-status;desc="MISS", titip-origin;dur=42.10, titip-store;dur=1.85;desc="lz4 (12.4KB -> 3.8KB, -69%)", titip;dur=44.15

Metrics Reported

Metric Description
titip-status;desc="..." Cache status (HIT, MISS, EXPIRED, REVALIDATED, STALE, DYNAMIC, BYPASS).
titip-meta;dur=X Stage 1 metadata lookup duration in milliseconds.
titip-body;dur=X[;desc="<codec>"] Stage 2 body retrieval and decompression duration. Appends ;desc="<codec>" (e.g. desc="lz4") when decompression is performed.
titip-origin;dur=X Upstream backend origin fetch duration (on misses or revalidations).
titip-store;dur=X[;desc="..."] Cache storage duration (compression + storage write). Reports codec, pre/post size, and reduction percentage: desc="<codec> (<rawSize> -> <compSize>, <savedPct>%)" (e.g. desc="lz4 (12.4KB -> 3.8KB, -69%)"), or desc="none (<rawSize>)".
titip-esi;dur=X;desc="N fragments" Edge Side Includes processing duration and fragment count.
titip;dur=X Total Titip processing duration from request arrival.

Tip

Titip emits Server-Timing via Header.Add, preserving any existing application-level Server-Timing headers sent by upstream backends (e.g. database query timings). Browsers combine them into a unified list.

Restricting Access via Debug Cookie

To prevent exposing internal infrastructure metrics to the general public, gate header generation with a cookie:

cache, err := titip.New(
    store,
    titip.WithServerTimingCookie("debug_timing", "secret_value"),
)

Storage Compression

Titip supports configurable internal storage compression to optimize storage memory consumption (e.g. Redis RAM) and network transfer:

  • "lz4" (default): Ultra-fast compression and decompression with minimal CPU overhead.
  • "zstd": Higher compression ratio with fast decompression and bounded memory footprint (512KB window).
  • "none": Uncompressed byte storage with zero CPU overhead.
  • Pluggable Codecs: Custom algorithms implementing the Compressor interface registered via WithStorageCompressor(comp).
cache, err := titip.New(
    store,
    titip.WithStorageCompression("zstd"),
)

Automatic Compression Bypass

Regardless of the selected compression algorithm, Titip automatically bypasses compression (storing raw bytes with "none") when:

  1. Payload size is < 256 bytes (avoiding compression frame overhead and size expansion).
  2. Origin response has a Content-Encoding header (e.g. gzip, br, zstd).
  3. Response is an inherently compressed media format (image/* except SVG/ICO, video/*, audio/*, application/zip, application/pdf).

Cache Invalidation & Purge API

Titip provides a programmatic Go API for Hierarchical Path Purging, Prefix Purging, Surrogate Tag Purging, and Namespace Invalidation. All purge methods return (int64, error), reporting the total number of logical cache entries invalidated.

Programmatic Go API

// Path Purge (purges /api/products and all its query string variants)
count, err := cache.Purge(ctx, "/api/products")

// Exact Query Invalidation (purges only ?id=10, leaves other queries intact)
count, err := cache.Purge(ctx, "http://example.com/api/products?id=10", titip.WithSoftPurge())

// Prefix Purge (purges all cached paths under /assets/, e.g. /assets/app.js)
count, err := cache.PurgePrefix(ctx, "/assets/")

// Surrogate Tag Invalidation (invalidates all cached entries matching the tag)
count, err := cache.PurgeTag(ctx, "catalog")

// Invalidates all cached entries
count, err := cache.PurgeAll(ctx)

Tiered & Targeted Cache-Control (RFC 9213)

Titip supports RFC 9213 Targeted Cache-Control, allowing backend origins to define separate caching rules for the edge/proxy layer versus end-user browsers.

Precedence Hierarchy (First Match Wins)

$$\text{\textbf{Titip-Cache-Control}} ;\longrightarrow; \text{\textbf{CDN-Cache-Control (RFC 9213)}} ;\longrightarrow; \text{\textbf{Cache-Control (RFC 9111)}}$$

HTTP/1.1 200 OK
Titip-Cache-Control: public, max-age=86400, stale-while-revalidate=3600
Cache-Control: private, no-store
  • Titip (Intermediary): Caches the response in storage for 24 hours (max-age=86400), shielding the origin from load.
  • Client (Browser): Receives Cache-Control: private, no-store, preventing sensitive data from persisting in local browser history.

Explicit Cache Opt-In

Titip never caches heuristically. An origin response is only stored in cache and served fresh if it explicitly specifies a freshness lifetime:

  • Cache-Control: max-age=<seconds> or s-maxage=<seconds>
  • Or a future Expires date header

Directives like Cache-Control: public without a max-age grant permission for shared caching, but have a freshness lifetime of 0 seconds and cannot serve fresh cache hits. Responses lacking an explicit freshness duration, or containing Set-Cookie, Cache-Control: private, or no-store, are treated as uncacheable (DYNAMIC) and served directly from origin without being saved to storage.

Edge Side Includes (ESI)

Titip includes an Edge Side Includes (ESI) engine with parallel fragment fetching, circular loop protection, and SSRF prevention.

When ESI is active, Titip advertises capability to upstream origins by sending Surrogate-Capability: titip="ESI/1.0" per Edge Side Includes (ESI) specifications. Origins can respond with Surrogate-Control: content="ESI/1.0" to direct ESI processing.

For standalone package documentation and options reference, see ESI Package Guide.

cache, err := titip.New(
    store,
    titip.WithESI(
        esi.WithInternalFetcher(esi.HandlerFetcher(router)),
        esi.WithMaxDepth(3),
        esi.WithMaxTimeout(5 * time.Second),
    ),
)

Supported ESI Tags & Syntax

Syntax Description
<esi:include src="/fragment" /> Self-closing fragment include. Fetched concurrently.
<esi:include src="/fragment" alt="/fallback" onerror="continue" /> Include with fallback URL on failure or silent omission (onerror="continue").
<esi:include src="...">Fallback HTML</esi:include> Paired include with inline fallback block.
<!--esi <div>Visible only when ESI active</div> --> Unescapes enclosed HTML comments when ESI is enabled.
<esi:remove><p>Placeholder</p></esi:remove> Strips placeholder content intended for non-ESI clients.
<!--esi-comment text="..." --> Strips internal comments without emitting bytes.

ESI Functional Options (esi.Option)

Option Builder Default Description
esi.WithHeaderRequired() (disabled) Process ESI only when origin sets Surrogate-Control: content="ESI/1.0".
esi.WithInternalFetcher(fn) nil Custom hook for in-memory virtual subrequests (e.g. esi.HandlerFetcher(r)).
esi.WithMaxDepth(uint32) 3 Maximum nesting depth for recursive ESI includes.
esi.WithMaxTimeout(duration) 30s Maximum time budget per fragment include fetch.
esi.WithMaxConcurrentRequests(int) 8 Maximum concurrent fetch goroutines per document.
esi.WithAllowPrivateIPs() (disabled) SSRF guard: allows requests to private, loopback, and link-local IP addresses.
esi.WithAllowedHosts(...string) [] List of allowed external hosts for domain includes (empty allows all public hosts).
esi.WithAllowPrivateIPsForAllowedHosts() (disabled) Permits private IPs specifically for explicitly allowed hosts.
esi.WithMaxResponseSize(int64) 10MB Maximum allowed fragment body size in bytes.
esi.WithoutForwardCookies() (forwarding enabled) Disables forwarding Set-Cookie headers from fragment responses to the client.
esi.WithPreserveETag() (disabled) Weakens origin ETag (W/"...") and preserves Last-Modified for downstream 304. By default, strips ETag/Last-Modified downstream to guarantee fresh fragment execution.
esi.WithIncludeErrorMarker(string) "" HTML placeholder rendered on unhandled fetch errors.

Observability & Metrics

Titip exports comprehensive Prometheus metrics for request traffic, cache latencies, purge invalidations, and ESI fragment processing:

// Register with a Prometheus registry:
cache, err := titip.New(
    store,
    titip.WithMetrics(prometheus.DefaultRegisterer),
)

Exported Metrics

Metric Name Type Labels Description
titip_requests_total Counter status (hit, miss, stale_hit, revalidated, bypass, error) Total HTTP requests processed by Titip caching middleware.
titip_request_duration_seconds Histogram status Request latency distribution in seconds across cache statuses.
titip_purges_total Counter type (url, tag, all), mode (hard, soft), status (success, error) Total purge operations executed by type and mode.
titip_purged_entries_total Counter type (url, tag, all), mode (hard, soft) Total logical cache entries invalidated by purge operations.
titip_esi_fragments_total Counter status (success, fallback, error) Total ESI fragment includes processed (enabled when ESI is active).
titip_esi_duration_seconds Histogram mode (in_process, outbound) Latency distribution of ESI fragment fetching and document splicing.
# Example Prometheus Scrape Output:
titip_requests_total{status="hit"} 4125
titip_requests_total{status="miss"} 102
titip_requests_total{status="stale_hit"} 18
titip_requests_total{status="revalidated"} 12
titip_requests_total{status="bypass"} 5
titip_requests_total{status="error"} 0

titip_purges_total{mode="soft",status="success",type="url"} 45
titip_purged_entries_total{mode="soft",type="url"} 45

titip_esi_fragments_total{status="success"} 230
titip_esi_duration_seconds_bucket{mode="in_process",le="0.005"} 225

Graceful Shutdown

Call cache.Close(ctx) during application teardown to cleanly drain background tasks:

// Wait for all in-flight background SWR revalidations to complete
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

if err := cache.Close(ctx); err != nil {
    log.Printf("cache close error: %v", err)
}

Close blocks until all active background stale-while-revalidate (SWR) goroutines have finished or the context expires, and closes the underlying storage driver if it implements storage.Closer or io.Closer.

Testing & Concurrency Standards

Titip is tested under continuous race detection and high-concurrency workloads:

# Run all unit and concurrency stress tests with race detector
go test -race -count=50 -v ./...

Contributing

We welcome contributions for new framework adapters and storage drivers.

Please read our Contributing Guide for architectural guidelines, interface contracts, and testing standards.

License

This project is licensed under the Apache 2.0 License.

About

High-throughput, low-allocation RFC-compliant HTTP caching middleware for Go and API gateways.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages