Skip to content

Connection & backends

go
redis, err := core.NewCache(env)          // uses CACHE_* / CACHE_CONNECTION_STRING
if err != nil {
    panic(err)
}
app, _ := core.NewApp(env, core.WithCache("default", redis))
defer app.Shutdown(context.Background())

NewCache PINGs before returning, so a misconfigured cache fails at boot rather than on the first request that touches it.

Three backends

ConstructorWhat it isUse it for
core.NewCache(env)redis (standalone, cluster or sentinel)anything with more than one replica
core.NewMemoryCache()in-processdev, tests, single-instance tools
core.NewNoopCache()stores nothingwhat an unconfigured service gets

The memory cache is a real cache — same encoding, real expiry, atomic counters, working locks and pub/sub — but it cannot span processes. Two replicas each get their own, so it is the wrong choice for anything that coordinates across instances: a rate limit that allows N per replica, a lock that locks nothing, an invalidation message half the fleet never hears.

go
c := core.NewMemoryCache()
defer c.Close()
app, _ := core.NewApp(env, core.WithCache("default", c))

Deployment shapes

The shape is configuration, not code — the same ICache comes back from all three:

Standalone

sh
CACHE_HOST=redis
CACHE_PORT=6379
CACHE_PASSWORD=secret
CACHE_DB=0

Cluster — several addresses, no master name:

sh
CACHE_ADDRS=redis-0:6379,redis-1:6379,redis-2:6379

Sentinel — addresses are the sentinels once a master name is set:

sh
CACHE_ADDRS=sentinel-0:26379,sentinel-1:26379,sentinel-2:26379
CACHE_MASTER_NAME=mymaster
CACHE_SENTINEL_PASSWORD=

A URI, which wins over the discrete fields:

sh
CACHE_CONNECTION_STRING=redis://:pass@host:6379/0
CACHE_CONNECTION_STRING=rediss://:pass@host:6380/0     # TLS

In a cluster, keys touched by one command must live on one slot. MGet, MSet and DelByPrefix across arbitrary keys are the ones to watch — the client handles the routing, but a command spanning slots is several round trips, not one.

TLS

sh
CACHE_TLS=true                 # with discrete fields
CACHE_TLS_SKIP_VERIFY=false    # self-signed certificates only

A URI uses rediss:// instead. CACHE_TLS_SKIP_VERIFY=true disables certificate verification — it is for a development instance with a self-signed certificate, and turning it on in production removes the only thing TLS was protecting against.

Pool and timeouts

sh
CACHE_POOL_SIZE=50
CACHE_MIN_IDLE_CONNS=5
CACHE_DIAL_TIMEOUT=5     # seconds; also bounds the PING at boot
CACHE_READ_TIMEOUT=3     # seconds
CACHE_WRITE_TIMEOUT=3    # seconds
CACHE_MAX_RETRIES=3      # -1 disables retrying

CACHE_READ_TIMEOUT is the important one. Without it, a redis that has stopped answering — but not closed the connection — holds every request that touches the cache until something else gives up. A cache is supposed to make requests faster; a cache with no read timeout can make every request in the service slower than it would be with no cache at all.

Every key and its default is in Configuration → Cache.

Namespacing

CACHE_PREFIX namespaces every key and every channel. Set it per service and per environment when instances share a redis, so a staging deploy cannot read — or invalidate — production's keys:

sh
CACHE_PREFIX=orders-staging

WithPrefix narrows further, which is how a per-tenant or per-feature namespace stays one:

go
otp := ctx.Cache().WithPrefix("otp")
otp.Set("0812345678", code, 5*time.Minute)   // "<CACHE_PREFIX>otp:0812345678"

n, _ := otp.DelByPrefix("")                  // clears only the otp namespace

A : is appended when the prefix does not already end in one. Prefix() returns what is being applied, and Redis() does not apply it — build keys with Prefix() when you drop to the raw client:

go
rdb := ctx.Cache().Redis()
rdb.XAdd(ctx, &redis.XAddArgs{Stream: ctx.Cache().Prefix() + "events", …})

Named connections

go
app, _ := core.NewApp(env,
    core.WithCache("default", sessions),
    core.WithCache("events", eventsRedis),
)
go
ctx.Cache()                // "default"
ctx.Caches("events")       // a named one
app.NewSubscriber(core.WithSubscriberCache("events"))

Worth doing when one instance holds small hot keys and another carries pub/sub traffic — a slow consumer on the second cannot then stall the first.

Health checks

go
if err := ctx.Cache().Ping(); err != nil {
    // readiness, not liveness
}

Think twice before failing readiness on the cache. If the service degrades correctly without it — which is what the disabled cache is designed for — taking the pod out of rotation converts a slow cache into an outage.

Custom context

Cache calls are bound to the request, so a cancelled request abandons them. When work must outlive the request, bind another context:

go
bg, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
core.GetJSON[User](ctx.Cache().WithContext(bg), "user:1")

Shutdown

app.Shutdown(ctx) closes the caches it was given, after stopping the subscribers that read from them. Handing a cache to core.WithCache transfers that responsibility — do not also Close() it.

Maintained by Passakon Puttasuwan & Dev Core Team.