cache

package module
v0.0.0-...-bfc21b5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 22, 2026 License: MIT Imports: 17 Imported by: 60

README

http-cache

Build Status Coverage Status

This is a high performance Golang HTTP middleware for server-side application layer caching, ideal for REST APIs.

It is simple, super fast, thread safe and gives the possibility to choose the adapter (memory, Redis, DynamoDB etc).

The memory adapter minimizes GC overhead to near zero and supports some options of caching algorithms (LRU, MRU, LFU, MFU). This way, it is able to store plenty of gigabytes of responses, keeping great performance and being free of leaks.

Getting Started

Installation

go get github.com/victorspringer/http-cache

Usage

This is an example of use with the memory adapter:

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/victorspringer/http-cache"
    "github.com/victorspringer/http-cache/adapter/memory"
)

func example(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Ok"))
}

func main() {
    memcached, err := memory.NewAdapter(
        memory.AdapterWithAlgorithm(memory.LRU),
        memory.AdapterWithCapacity(10000000),
    )
    if err != nil {
        log.Fatal(err)
    }

    cacheClient, err := cache.NewClient(
        cache.ClientWithAdapter(memcached),
        cache.ClientWithTTL(10 * time.Minute),
        cache.ClientWithRefreshKey("opn"),
    )
    if err != nil {
        log.Fatal(err)
    }

    handler := http.HandlerFunc(example)

    http.Handle("/", cacheClient.Middleware(handler))
    http.ListenAndServe(":8080", nil)
}

Example of Client initialization with Redis adapter:

import (
    "github.com/victorspringer/http-cache"
    "github.com/victorspringer/http-cache/adapter/redis"
)

...

    ringOpt := &redis.RingOptions{
        Addrs: map[string]string{
            "server": ":6379",
        },
    }
    cacheClient, err := cache.NewClient(
        cache.ClientWithAdapter(redis.NewAdapter(ringOpt)),
        cache.ClientWithTTL(10 * time.Minute),
        cache.ClientWithRefreshKey("opn"),
    )

...

Optional Features

Programmatic invalidation

Use Drop to release the cached response that matches a request. The same cache key rules used by the middleware are applied, including normalized query params, POST bodies and configured vary headers.

req, err := http.NewRequest(http.MethodGet, "https://example.com/products?page=1", nil)
if err != nil {
    log.Fatal(err)
}

if err := cacheClient.Drop(req); err != nil {
    log.Fatal(err)
}
PURGE requests

PURGE support is opt-in so existing applications that already handle PURGE keep working as before.

Authentication is your responsibility. This middleware does not authenticate PURGE requests; any caller that can reach the endpoint can flush cache entries. The same applies to ClientWithRefreshKey — any query string carrying the configured key will release the matching entry. Place an auth middleware (basic auth, signed token, source-IP allowlist, etc.) in front of the cache middleware before exposing either feature on a public surface.

cacheClient, err := cache.NewClient(
    cache.ClientWithAdapter(memcached),
    cache.ClientWithTTL(10 * time.Minute),
    cache.ClientWithPurge(),
)

When enabled, a matching PURGE request releases the cached response and returns 204 No Content. For endpoints cached by POST body, send the PURGE request with the same body so the cache keys match.

Observability

Use ClientWithObserver to receive cache middleware events. The event includes the request, cache key, event type and status code when available.

cacheClient, err := cache.NewClient(
    cache.ClientWithAdapter(memcached),
    cache.ClientWithTTL(10 * time.Minute),
    cache.ClientWithObserver(func(event cache.CacheEvent) {
        log.Printf("cache event=%s key=%d status=%d path=%s",
            event.Type,
            event.Key,
            event.StatusCode,
            event.Request.URL.Path,
        )
    }),
)

Available event types are hit, miss, stale, refresh, store and purge.

Cache key and storage options
  • ClientWithMethods enables caching for GET and/or POST requests.
  • ClientWithVaryHeaders includes selected request headers in the cache key.
  • ClientWithStatusCodeFilter controls which response status codes can be cached.
  • ClientWithSkipCacheResponseHeader skips storage when a response includes a configured header.
  • ClientWithSkipCacheURIPathRegex skips lookup and storage for matching URL paths.
  • ClientWithExpiresHeader writes the cached response expiration as an Expires header.
  • ClientWithMaxBodySize(n) caps the response body bytes the middleware will buffer and cache. Responses larger than n are still streamed to the client untouched, but their buffered copy is dropped and the entry is not stored. Recommended for any endpoint that can emit large payloads (downloads, streaming responses).
Cache stampede protection

ClientWithSingleflight coalesces concurrent misses for the same cache key so the origin handler runs only once per stampede. All concurrent callers receive the same response. Disabled by default — opt in if your origin is expensive enough that an N-way concurrent miss is a real concern.

cacheClient, err := cache.NewClient(
    cache.ClientWithAdapter(memcached),
    cache.ClientWithTTL(10 * time.Minute),
    cache.ClientWithSingleflight(),
)
Respecting Cache-Control

ClientWithRespectCacheControl makes the middleware honor a useful subset of RFC 7234:

  • Request Cache-Control: no-store → bypass the cache entirely (no lookup, no store).
  • Request Cache-Control: no-cache → skip the lookup; the handler runs against the origin and the response is still stored.
  • Response Cache-Control: no-store, no-cache, or private → do not store the response (shared-cache semantics).
  • Response Cache-Control: s-maxage=N / max-age=N → override the client default TTL for this single response (s-maxage wins).

Unknown directives are intentionally ignored, so any extension your application emits keeps its existing behavior.

Stale-while-revalidate

ClientWithStaleWhileRevalidate(window) implements RFC 5861 stale-while-revalidate. An expired entry whose age is no greater than window is served from cache immediately while a single background goroutine refreshes it. Concurrent stale hits coalesce to one origin call. The refresh outlives the triggering request, so a client disconnect does not abort the refill.

cacheClient, err := cache.NewClient(
    cache.ClientWithAdapter(memcached),
    cache.ClientWithTTL(1 * time.Minute),
    cache.ClientWithStaleWhileRevalidate(10 * time.Second),
)

Benchmarks

The benchmarks were based on allegro/bigcache tests and used to compare it with the http-cache memory adapter.
The tests were run using an Intel i5-2410M with 8GB RAM on Arch Linux 64bits.
The results are shown below:

Writes and Reads
cd adapter/memory/benchmark
go test -bench=. -benchtime=10s ./... -timeout 30m

BenchmarkHTTPCacheMamoryAdapterSet-4             5000000     343 ns/op    172 B/op    1 allocs/op
BenchmarkBigCacheSet-4                           3000000     507 ns/op    535 B/op    1 allocs/op
BenchmarkHTTPCacheMamoryAdapterGet-4            20000000     146 ns/op      0 B/op    0 allocs/op
BenchmarkBigCacheGet-4                           3000000     343 ns/op    120 B/op    3 allocs/op
BenchmarkHTTPCacheMamoryAdapterSetParallel-4    10000000     223 ns/op    172 B/op    1 allocs/op
BenchmarkBigCacheSetParallel-4                  10000000     291 ns/op    661 B/op    1 allocs/op
BenchmarkHTTPCacheMemoryAdapterGetParallel-4    50000000    56.1 ns/op      0 B/op    0 allocs/op
BenchmarkBigCacheGetParallel-4                  10000000     163 ns/op    120 B/op    3 allocs/op

http-cache writes are slightly faster and reads are much more faster.

Garbage Collection Pause Time
cache=http-cache go run benchmark_gc_overhead.go

Number of entries:  20000000
GC pause for http-cache memory adapter:  2.445617ms

cache=bigcache go run benchmark_gc_overhead.go

Number of entries:  20000000
GC pause for bigcache:  7.43339ms

http-cache memory adapter takes way less GC pause time, that means smaller GC overhead.

Roadmap

  • Develop gRPC middleware
  • Develop Badger adapter
  • Develop DynamoDB adapter
  • Develop MongoDB adapter

Godoc Reference

License

http-cache is released under the MIT License.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func KeyAsString

func KeyAsString(key uint64) string

KeyAsString can be used by adapters to convert the cache key from uint64 to string.

Types

type Adapter

type Adapter interface {
	// Get retrieves the cached response by a given key. It also
	// returns true or false, whether it exists or not.
	Get(key uint64) ([]byte, bool)

	// Set caches a response for a given key until an expiration date.
	Set(key uint64, response []byte, expiration time.Time)

	// Release frees cache for a given key.
	Release(key uint64)
}

Adapter interface for HTTP cache middleware client.

type AdapterTouch

type AdapterTouch interface {
	Touch(key uint64)
}

AdapterTouch is an optional Adapter extension. When an adapter implements it, the middleware records each cache hit via Touch instead of re-encoding the response and calling Set, eliminating the read-modify-write lost-update race and the per-hit gob round-trip. Adapters that do not implement Touch keep receiving the legacy Set call so existing custom adapters retain their behavior.

type CacheEvent

type CacheEvent struct {
	Type       CacheEventType
	Request    *http.Request
	Key        uint64
	StatusCode int
}

CacheEvent is passed to an observer when cache middleware events happen.

type CacheEventType

type CacheEventType string

CacheEventType identifies an observed cache middleware event.

const (
	// CacheEventHit means a valid cached response was served.
	CacheEventHit CacheEventType = "hit"

	// CacheEventMiss means no cached response was found.
	CacheEventMiss CacheEventType = "miss"

	// CacheEventStale means an expired cached response was found and released.
	CacheEventStale CacheEventType = "stale"

	// CacheEventRefresh means a cached response was explicitly released.
	CacheEventRefresh CacheEventType = "refresh"

	// CacheEventStore means a response was stored in cache.
	CacheEventStore CacheEventType = "store"

	// CacheEventPurge means a cached response was explicitly purged.
	CacheEventPurge CacheEventType = "purge"
)

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client data structure for HTTP cache middleware.

func NewClient

func NewClient(opts ...ClientOption) (*Client, error)

NewClient initializes the cache HTTP middleware client with the given options.

func (*Client) Drop

func (c *Client) Drop(r *http.Request) error

Drop releases the cache entry matching the given request. The caller's *http.Request is left unmodified: its URL.RawQuery is not reordered and its Body remains readable after the call returns.

func (*Client) Middleware

func (c *Client) Middleware(next http.Handler) http.Handler

Middleware is the HTTP cache middleware handler.

type ClientOption

type ClientOption func(c *Client) error

ClientOption is used to set Client settings.

func ClientWithAdapter

func ClientWithAdapter(a Adapter) ClientOption

ClientWithAdapter sets the adapter type for the HTTP cache middleware client.

func ClientWithExpiresHeader

func ClientWithExpiresHeader() ClientOption

ClientWithExpiresHeader enables middleware to add an Expires header to responses. Optional setting. If not set, default is false.

func ClientWithMaxBodySize

func ClientWithMaxBodySize(size int) ClientOption

ClientWithMaxBodySize caps the response body bytes the middleware will buffer and cache. Responses that grow past the limit are still streamed to the client untouched, but their buffered copy is dropped and the entry is not stored. Defaults to no limit (0) for backward compatibility; that default lets a single oversized response (a download, an unbounded stream) hold its bytes in memory until the handler returns, so set a cap for any endpoint that can emit large payloads.

func ClientWithMethods

func ClientWithMethods(methods []string) ClientOption

ClientWithMethods sets the acceptable HTTP methods to be cached. Optional setting. If not set, default is "GET".

func ClientWithObserver

func ClientWithObserver(observer Observer) ClientOption

ClientWithObserver sets a function that receives cache middleware events. Optional setting.

func ClientWithPurge

func ClientWithPurge() ClientOption

ClientWithPurge enables handling PURGE requests by releasing matching cache entries. Optional setting.

func ClientWithRefreshKey

func ClientWithRefreshKey(refreshKey string) ClientOption

ClientWithRefreshKey sets the parameter key used to free a request cached response. Optional setting.

func ClientWithRespectCacheControl

func ClientWithRespectCacheControl() ClientOption

ClientWithRespectCacheControl makes the middleware honor a small but useful subset of RFC 7234 Cache-Control directives on both requests and responses:

  • Request Cache-Control: no-store -> bypass the cache entirely (no lookup, no store).
  • Request Cache-Control: no-cache -> skip the cache lookup so the handler always runs, but still store the response.
  • Response Cache-Control: no-store, no-cache, or private -> do not store the response.
  • Response Cache-Control: s-maxage=N / max-age=N -> override the client's default TTL for this response (s-maxage wins, matching shared-cache semantics).

Defaults off so existing applications that emit Cache-Control purely for downstream clients see no behavior change.

func ClientWithSingleflight

func ClientWithSingleflight() ClientOption

ClientWithSingleflight coalesces concurrent cache misses for the same key so the origin handler runs only once per cache-miss batch. All concurrent callers receive the same response. Defaults off so callers who depend on per-request handler invocations are not surprised.

func ClientWithSkipCacheResponseHeader

func ClientWithSkipCacheResponseHeader(header string) ClientOption

ClientWithSkipCacheResponseHeader sets a response header that prevents successful responses from being stored.

func ClientWithSkipCacheURIPathRegex

func ClientWithSkipCacheURIPathRegex(pathRegex *regexp.Regexp) ClientOption

ClientWithSkipCacheURIPathRegex skips cache lookup and storage for matching request URL paths.

func ClientWithStaleWhileRevalidate

func ClientWithStaleWhileRevalidate(window time.Duration) ClientOption

ClientWithStaleWhileRevalidate enables RFC 5861 stale-while-revalidate semantics: an expired entry whose Expiration is no older than window is served from cache immediately while a single background goroutine refreshes it from the origin. Concurrent stale hits coalesce so only one refresh runs per key at a time. Defaults to 0 (off), in which case expired entries continue to fall through to the existing release-and-miss path.

func ClientWithStatusCodeFilter

func ClientWithStatusCodeFilter(filter func(int) bool) ClientOption

ClientWithStatusCodeFilter sets the response status codes that can be cached. Optional setting. If not set, responses below 400 are cached.

func ClientWithTTL

func ClientWithTTL(ttl time.Duration) ClientOption

ClientWithTTL sets how long each response is going to be cached.

func ClientWithVaryHeaders

func ClientWithVaryHeaders(headers []string) ClientOption

ClientWithVaryHeaders includes selected request headers in cache keys.

type Observer

type Observer func(CacheEvent)

Observer receives cache middleware events.

type Response

type Response struct {
	// Value is the cached response value.
	Value []byte

	// Header is the cached response header.
	Header http.Header

	// Expiration is the cached response expiration date.
	Expiration time.Time

	// LastAccess is the last date a cached response was accessed.
	// Used by LRU and MRU algorithms.
	LastAccess time.Time

	// Frequency is the count of times a cached response is accessed.
	// Used for LFU and MFU algorithms.
	Frequency int

	// CanonicalKey is a fingerprint of the request inputs (URL, vary
	// headers and body) that produced this entry. The middleware
	// compares it against the incoming request on every hit so an
	// FNV-64 collision (or a manually corrupted entry) cannot serve
	// one client's response to another. Entries written by older
	// versions of this package have an empty CanonicalKey and bypass
	// verification for backward compatibility.
	CanonicalKey []byte
}

Response is the cached response data structure.

func BytesToResponse

func BytesToResponse(b []byte) Response

BytesToResponse converts bytes array into Response data structure. Decoding errors are silently swallowed for backward compatibility; the middleware uses decodeResponse internally so it can detect corruption and treat the entry as a cache miss.

func (Response) Bytes

func (r Response) Bytes() []byte

Bytes converts Response data structure into bytes array.

func (Response) Valid

func (r Response) Valid() bool

Valid returns whether the response can still be served from cache.

Directories

Path Synopsis
adapter

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL