Managed Redis in one minute
Managed Redis in one minute
Section titled “Managed Redis in one minute”The hosted server reads non-secret host capabilities from
.github/workflows/cao.json. Redis credentials stay in deployment environment
variables. Redis provider modules only select conventional environment-variable names
and consistency constraints;
they do not add provider SDKs or weaken TLS verification.
Configure a Postgres connection separately for current dashboard entity storage
and queries. Redis supports operational caches, queues, and sessions; the
server does not store dashboard entities, RediSearch
indexes, or generations in Redis. Postgres replacements are transactional and
have no generations, projections, or snapshots.
To keep hosting settings separate from rollout policy, you can instead use a
reviewed deployment-specific host extension.
1. Add the host policy
Section titled “1. Add the host policy”Add control-plane.web.host to cao.json:
{ "target": { "module": "container", "name": "managed-redis" }, "redis": { "module": "generic", "url-env": "REDIS_URL", "namespace-env": "REDIS_NAMESPACE", "session": "pooled", "tls": { "mode": "required", "server-name-env": "REDIS_TLS_SERVER_NAME", "ca-certificate-env": "REDIS_TLS_CA_CERT" } }}The container includes the checked-in policy. For another location, set
CAO_POLICY_PATH to the mounted cao.json. REDIS_URL accepts
redis:// or rediss://; tls.mode: "required" establishes verified TLS even
when the supplied URL uses redis://. The server derives the certificate name
from the URL host unless REDIS_TLS_SERVER_NAME is set. Put PEM certificate
contents—not a path—in REDIS_TLS_CA_CERT.
Use tls.mode: "disabled" only with a private endpoint and
allow-private-plaintext: true. Public plaintext endpoints are rejected.
The target and Redis provider are independent modules. Change target.module
without changing the Redis connection, or change redis.module without changing
the app server target. Built-in targets are container and azure-functions;
generic accepts explicit OAuth, listener, proxy,
and single-replica capabilities for a new platform.
Module deployment flow
Section titled “Module deployment flow”- Policy validation rejects unknown modules and provider-owned capability overrides.
- The target module resolves authentication, listener ownership, HTTPS, and proxy trust.
- The Redis module resolves environment names, TLS minimums, connection semantics, replica constraints, and collection support.
- Startup composes both modules into one host profile, constructs the generic Redis client, and asserts that the client and runtime match every capability.
- The selected target adapter starts the process listener or delegates it to the platform. Readiness must succeed before deployment promotion.
Target modules never resolve Redis credentials. Redis modules never own HTTP request handling or deployment authority. Adding a module requires a reviewed registry entry, schema and policy validation, documentation, and composition tests; modules cannot be loaded from an untrusted path at runtime.
Hosted startup requires this modular web.host policy. Flat host objects,
redis.preset, and environment-only host selection are rejected.
| App target module | Use |
|---|---|
container | Railway, Render, Kubernetes, VMs, and similar process-owned hosts. |
azure-functions | Azure Functions’ platform-owned listener and trusted platform proxy. |
generic | A new platform with explicit reviewed capabilities. |
Externally owned Go HTTP listener
Section titled “Externally owned Go HTTP listener”For an HTTP server that owns its own socket and shutdown, select the
generic target with authentication: "github-oauth" and
listener: "external" in a reviewed host extension; keep the Redis module
independent:
{ "target": { "module": "generic", "name": "external-host", "authentication": "github-oauth", "listener": "external" }, "redis": { "module": "redis-cloud", "tls": { "mode": "required" } }}The public server/hosting package accepts built-site and query-document
paths, then offers New, Start, Handler, and Stop. Start CAO before
admitting requests; shut down and drain the HTTP server before calling Stop.
Only the host owns the socket and TLS certificates: a CAO process listen address
or TLS files are rejected. The host must pass the complete handler unchanged;
CAO still verifies OAuth, session/CSRF, webhook signatures, request hosts, and
rate limits. Direct requests require HTTPS, or the host can use a trusted
private proxy with CAO_TRUSTED_PROXY_CIDRS and an exact CAO_ALLOWED_HOSTS
allowlist. Forwarded headers from other peers are rejected.
Preserve the root paths and streaming support, including http.Flusher and
response deadline control. /api/v1/events is long-lived, so a global
whole-request timeout cannot encompass it; apply bounded deadlines to ordinary
routes instead. Bound concurrent connections and streams at the host because
per-request rate limiting alone does not cap active streams. Public package
builds require no host framework or private dependency.
Example compositions
Section titled “Example compositions”Container platforms pair the container target with any
compatible Redis module:
{ "target": { "module": "container", "name": "hosted-dashboard" }, "redis": { "module": "redis-cloud", "tls": { "mode": "required" } }}Azure Functions can use the same Redis modules without changing its listener adapter:
{ "target": { "module": "azure-functions" }, "redis": { "module": "gcp-memorystore", "tls": { "mode": "required" } }}Local development remains the cao-dashboard serve example with loopback Redis;
the local Redis module describes its ordinary pooled semantics. Upstash pairs
the container target with the constrained upstash Redis module; see the
Upstash deployment guide.
Redis memory budget
Section titled “Redis memory budget”All server profiles default to a 200 MB (200,000,000-byte) whole-node Redis
memory budget. Set CAO_REDIS_MAX_BYTES to a positive byte count to change it;
Go embedders can override the environment with server.Config.RedisMaxBytes.
Malformed, zero, and negative values fail configuration. Azure has no permanent
free Redis tier: this default leaves headroom on the smallest Azure Cache for
Redis tier (250 MB), not a free-service guarantee.
Cache pressure measures Redis INFO memory usage, including operational state,
connection buffers, and Redis overhead. If the provider reports maxmemory, the
effective budget is the smaller of the configured budget and 80% of
maxmemory. Azure Managed Redis may omit maxmemory; the finite configured
CAO budget still applies, but an unknown provider quota is not reported as an
unlimited instance. Check the SKU capacity in the provider management plane.
Query-result writes atomically retire expired and oldest cached
results before admission, and remeasure after writing. Marketplace and
repository-memory cache writes atomically decline admission when there is not
enough headroom and discard a newly written entry if its actual allocation
exceeds the budget. Before copying or returning a cached payload, reads reserve
conservative outgoing-response headroom against the global budget. That reserve
is separate from the local stored-query limit and does not renew cache TTLs.
Unsafe reads become ordinary cache misses. Rejected admissions are logged; they
do not discard the freshly computed response.
Startup probes the scripting capabilities, not the provider name. Redis
Software/Cloud prohibit INFO inside Lua,
including Azure Managed Redis’s Enterprise-based engine.
On these providers, disposable-cache admission and serving are explicitly
disabled, with a sanitized startup warning. Protected storage and fresh
source evaluation remain enabled. Pressure measurement and reclamation use
INFO memory outside Lua and allowlisted deletions. This is intentional
fail-closed degradation of optional caches, not independently sampled concurrent
admission or an unbounded compatibility cache. Missing or malformed required
memory statistics still fail maintenance explicitly.
Startup and a cancellation-scoped 30-second maintenance loop also enforce
the budget without requiring cache traffic. Maintenance trims query results
first, then traverses the namespace once with a bounded SCAN COUNT 1024 hint.
Only allowlisted marketplace and repository-memory digest keys may be deleted.
Empty or nonmatching batches also refresh pressure, and final statistics are
always refreshed before reporting an unattainable budget. Existing cache TTLs
are preserved. ACLs must allow external INFO memory, SCAN, and DEL; enabled
caches additionally need the script commands described below.
This is an automatic cache-pressure target, not permission to discard required
operational state or a hard limit on process RSS, disk persistence, or other
applications. Sessions, queues, quota state, revocation retries, and other
namespaces are never evicted. Redis maxmemory and eviction policy are never
changed. If these protected allocations alone exceed the budget, startup fails
explicitly, running maintenance reports memory pressure, and further cache
admissions are declined. INFO memory is node-wide and does not identify which
protected key families or other Redis users account for excess usage; an
eviction count covers only disposable caches reclaimed by CAO. Before changing
capacity, inspect provider-side memory and namespace/key-family usage. Do not
flush keys or reduce sessions, queues, quota state, or other protected state
blindly. Then scale Redis as needed and set CAO_REDIS_MAX_BYTES consistently
for the dashboard, collector, and backfill. Keep the effective budget within
80% of reported Redis maxmemory, use the same budget across replicas sharing a
Redis node, and retain the provider’s noeviction policy. Scaling the instance
does not automatically raise CAO_REDIS_MAX_BYTES; change both when required.
The read-only cao-dashboard doctor memory check resolves the same configuration
and reports node-wide used memory, provider limit, configured and effective
budgets, utilization, and policy.
Bounded query-result caching
Section titled “Bounded query-result caching”HTTP and MCP queries share a Redis cache-aside layer. Queries taking at least
100 ms are admitted only when their output fits both a conservative in-memory
size preflight and the compact JSON byte limit. Defaults are 1 MiB per result,
64 MiB total, and 1,024 entries. Set
CAO_QUERY_CACHE_MAX_RESULT_BYTES and CAO_QUERY_CACHE_MAX_BYTES to positive byte
counts before starting any server profile; the per-result limit cannot exceed
the total. Go embedders can additionally use server.Config.QueryCache to
disable caching or adjust the minimum execution duration.
Set CAO_QUERY_CACHE_DISABLED=true to disable result caching at startup without
changing application code. This emergency switch does not disable Redis-backed
authentication, request limits, or other operational dependencies. Invalid
switch values fail startup. Cache debug logs report the effective admission
threshold and limits at startup.
Query ETags are SHA-256 hashes of definitions, parameters, pagination, schema version, and authorization class, but not data revisions. Cached evidence keeps its original revision and evaluation time for a fixed five-minute TTL. Ingestion, rebuilds, and result reads do not invalidate or renew entries. Readiness and canonical entity APIs remain uncached. ETags index the internal result cache; the HTTP query response contract is unchanged.
A namespaced Redis hash holds only bounded result documents, with a sorted-set
expiry index using Redis server time. One Lua operation prunes expired entries
and evicts the oldest admissions until MEMORY USAGE for both keys is within
the budget. Budget reductions evict proportional oldest-first batches and
remeasure actual allocation, avoiding a full hash/index memory scan after every
individual eviction. Both keys also expire after five minutes without new admissions.
This bounds live result and index allocation, including metadata; it is a
separate per-cache limit; the whole-node budget above also applies. No global
maxmemory or eviction policy is changed, and session/queue keys are untouched.
Partial key eviction discards the remaining cache structure rather than losing
its accounting. Redis errors surface as a query 503, never as empty results or
an unbounded local-cache fallback.
For enabled caches, restricted Redis ACLs must allow the cache’s EVAL and the commands called by
its script: TIME, EXISTS, DEL, MEMORY USAGE, HGET, HSET, HDEL,
HSTRLEN, HEXISTS, ZADD, ZREM, ZRANGE, ZRANGEBYSCORE, ZSCORE,
ZCARD, PEXPIRE, INFO memory, and STRLEN, scoped to the application namespace
where applicable.
Enable cache debug logs with DEBUG=cao:server:query-cache (or
DEBUG=cao:server:*). Logs contain only fixed decisions, timings, byte counts,
entry counts, expiry, and eviction counts—not query ETags, parameters, results,
Redis keys, or errors containing connection details.
The existing standard OTEL_* configuration exports
cao_dashboard.query.cache.lookup and .store spans. Metrics under
cao_dashboard.query.cache. include operations and duration by fixed
operation/outcome, result.bytes, last-observed memory.bytes and entries,
and retired counts by expired/evicted. Lookup outcomes distinguish
hit/miss/error; admissions distinguish stored/result-size/capacity/error;
bypasses identify cheap queries, disabled caching, missing Redis, and readiness
probes. Occupancy gauges are observations at cache operations, not continuously
polled Redis state.
Run the cache integration regressions and bounded performance samples against an isolated local Redis with:
REDIS_URL=redis://127.0.0.1:6379/0 go -C server test ./internal/redisx \ -run '^TestQueryCache' -bench '^BenchmarkQueryCache' -benchmem -benchtime=5xThe benchmarks measure a full 1,024-entry hit and a full-cache budget reduction. They exclude fixture ingestion from the measured time. The regression suite checks measured allocator memory, namespace isolation, unrelated operational keys, concurrent duplicate admissions, expiry, malformed replies, and retirement under a one-second operation deadline.
2. Pick a Redis provider module
Section titled “2. Pick a Redis provider module”Change only redis.module, then map the provider connection into the listed
variables. Percent-encode usernames and passwords placed in a URL.
AWS ElastiCache
Section titled “AWS ElastiCache”{ "module": "aws-elasticache", "tls": { "mode": "required" } }REDIS_URL='rediss://PRIMARY_OR_CONFIGURATION_ENDPOINT:PORT'Use the endpoint and port reported by ElastiCache. Add URL credentials only when the cache uses RBAC. Serverless caches require in-transit encryption.
Redis Cloud
Section titled “Redis Cloud”{ "module": "redis-cloud", "tls": { "mode": "required", "ca-certificate-env": "REDIS_TLS_CA_CERT" } }REDIS_URL='rediss://DATABASE_ENDPOINT:PORT'Use the database security page’s CA PEM in REDIS_TLS_CA_CERT only when Redis
Cloud supplies a private CA.
GCP Memorystore
Section titled “GCP Memorystore”The module can assemble a URL from REDISHOST, REDISPORT,
REDIS_USERNAME, and REDIS_PASSWORD.
{ "module": "gcp-memorystore", "tls": { "mode": "required", "ca-certificate-env": "REDIS_TLS_CA_CERT" } }REDISHOST="$(gcloud redis instances describe INSTANCE --region=REGION --format='value(host)')"REDISPORT="$(gcloud redis instances describe INSTANCE --region=REGION --format='value(port)')"REDIS_TLS_CA_CERT="$(gcloud redis instances describe INSTANCE --region=REGION --format='value(serverCaCerts[0].cert)')"export REDISHOST REDISPORT REDIS_TLS_CA_CERTIf AUTH is enabled, set REDIS_PASSWORD to the instance AUTH string. Use
tls.mode: "disabled" plus allow-private-plaintext: true only for a private
non-TLS instance.
Railway
Section titled “Railway”{ "module": "railway", "tls": { "mode": "disabled" }, "allow-private-plaintext": true }In the CAO service variables:
REDIS_URL=${{Redis.REDIS_URL}}Use the actual Redis service name. Prefer Railway’s private URL; do not expose a TCP proxy solely for CAO.
Render
Section titled “Render”For a service in the same Render private network:
{ "module": "render", "tls": { "mode": "disabled" }, "allow-private-plaintext": true }REDIS_URL=<Render Internal Redis URL>For an external client, use the External Redis URL and
tls.mode: "required".
DigitalOcean Managed Valkey
Section titled “DigitalOcean Managed Valkey”{ "module": "digitalocean", "tls": { "mode": "required" } }REDIS_URL=<TLS connection URI from Connection Details>Use the reported URI and port rather than assuming a default.
Generic connection fields
Section titled “Generic connection fields”| Field | Default | Purpose |
|---|---|---|
module | Required | Provider environment mapping and fixed consistency capabilities. |
url-env | Module-specific, usually REDIS_URL | Complete Redis URL. |
host-env, port-env | Module-specific | Build a URL when no URL variable is supplied. |
username-env, password-env | Preset-specific | Credentials used only while building a URL. |
namespace-env | REDIS_NAMESPACE | Stable deployment identity; defaults to hosted-dashboard. |
session | pooled | pooled or fail-closed serialized. |
isolate-process-namespace | false | Use a fresh namespace each process start. |
allow-private-plaintext | false | Permit plaintext only for a private host. |
tls.mode | auto | Infer from URL, require TLS, or disable TLS. |
The upstash module fixes serialized sessions, process namespace isolation, one
replica, required TLS, and disabled server-side collection. The generic module
is the only Redis module that accepts explicit consistency capability fields.
Local Redis, Azure Functions, and Upstash are compositions of target and
Redis modules; they are not separate shared request implementations.
Verify
Section titled “Verify”Start the server and check:
curl -fsS https://YOUR_HOST/api/healthcurl -fsS https://YOUR_HOST/api/readinessBoth endpoints reflect dependency state, including Postgres data readiness and Redis operations. Never use them as process-only liveness probes.