Skip to main content
Deploying to Vercel? Consider Vercel KV for edge-compatible storage without managing Redis infrastructure.

Requirements

Development Setup

Projects created with frontmcp create --target node include Docker files in the ci/ folder:
When running inside Docker, use redis (the service name) as REDIS_HOST, not localhost. Projects created with frontmcp create include a ci/.env.docker file with Docker-specific values. It is git-ignored because it holds MCP_SESSION_SECRET; the committed ci/.env.docker.example is its template, so copy it to ci/.env.docker after cloning.
The compose file publishes Redis on 127.0.0.1:6379 only, so a server running on the host can reach it while other machines cannot. The app service runs the production image with NODE_ENV=production by default, which requires MCP_SESSION_SECRET: set it in ci/.env.docker (or export it in your shell, which wins over the file; openssl rand -hex 32). Compose reads ci/.env.docker only through --env-file ci/.env.docker, which the docker:* scripts pass, and refuses to start any service, Redis included, while the secret is unset. The generated ci/Dockerfile runs as the unprivileged node user and declares a HEALTHCHECK against /health, which the server answers whatever health.healthzPath says.

Option 2: Local Installation

Configuration

Already configuring the top-level redis block (or redis.provider: 'vercel-kv') on @FrontMcp? Set CachePlugin.init({ type: 'global-store' }) so the plugin automatically reuses that store without duplicating credentials.
For complete CachePlugin API documentation, see the Cache Plugin Guide.

Production Setup

Redis is required in production. Without Redis, session data will be lost on restart and caching will not persist across instances.

Self-Hosted Production

For self-hosted Redis in production:

Security Best Practices

  1. Authentication: Always set REDIS_PASSWORD in production
  2. Network: Run Redis in private network, not exposed to internet
  3. TLS: Enable TLS for encrypted connections
  4. Persistence: Use AOF for durability (appendonly yes)

Configuration Example

Switch the redis block to { provider: 'vercel-kv' } when deploying on Vercel KV—the Cache Plugin will keep reusing the same store without code changes. A managed Redis usually hands you a single connection URL. Pass it as url instead of the separate fields:
The URL is read into host, port, password and db when the config is parsed; rediss:// turns tls on, and ?db=2 and ?password= work when the path or the user part carries none. The user part may be empty or default (Redis ACL users other than default are refused with a validation error). Percent-encode reserved characters in the password (@ as %40, % as %25); a malformed escape such as %ZZ is a validation error that does not echo the password. keyPrefix and defaultTtlMs go next to url as usual. The URL is the base. host, port, password, db and tls written beside it only fill in what the URL leaves out — { url: 'redis://cache:6379', password: process.env.REDIS_PASSWORD } connects to cache:6379 with that password, and tls: true turns TLS on for a redis:// URL. A field that contradicts the URL ({ url: 'redis://cache:6379', host: 'other' }, a different port, password or database, or tls: false with rediss://) is a validation error naming the field, never silently dropped. A URL that names only the user (redis://default@cache) leaves the password out, so a password beside it is used. The same rule applies to pubsub, to throttle.storage’s redis block and to the job stores’ store.redis. Up to 1.9.2 the server’s redis option dropped a ?password=, and a URL given to throttle.storage kept the empty password of redis://default@cache over the password beside it.

Running more than one instance

Redis lets several instances share sessions, but each instance still signs and encrypts with its own secrets. Give every instance the same values: An anonymous session minted by one instance is honored by any instance with the same MCP_SESSION_SECRET: the instance that receives the request recreates the transport from the stored session (in distributed mode it relays to, or takes over from, the node recorded with that session). Before an instance serves a session it holds in memory, it checks that the session’s stored record still exists, so a session deleted or expired through one instance is dropped on the others too. That check waits at most transport.persistence.sessionCheckTimeoutMs (default 500) for Redis. If Redis accepts the connection but doesn’t answer (a paused container, a network partition that doesn’t reset TCP, a long-running command), the instance serves the session from memory, logs [TransportService] Could not confirm the session is still stored — serving it here with The session store did not answer within 500 ms, and checks again on the session’s next request. In 1.9.3 such a request waited until Redis answered.

When Redis is unreachable at startup

Not every Redis consumer reacts the same way when Redis is down as the server starts:
  • redis and transport.persistence log the failure ([TransportService] Failed to connect to redis - session persistence disabled) and the server starts. The connection is retried in the background with exponential backoff (1s doubling to 30s); once Redis is reachable, sessions are persisted again and /readyz turns healthy without a restart. Until then, sessions live on the instance that created them.
  • Every Redis connection FrontMCP opens reconnects on its own and logs connection errors at a rate-limited interval (with a count of suppressed repeats) rather than once per retry.
  • throttle.storage fails closed: rate limits are a security control, so startup aborts with GuardStorageUnavailableError (throttle.storage (redis) is unavailable: …). This is the default in production, where storage fallback defaults to 'error'. If Redis goes away while the server is running, a rate-limited call is refused with the same GuardStorageUnavailableError rather than an internal error (HTTP 503 from the throttle.global check; an isError result with _meta.code: 'GUARD_STORAGE_UNAVAILABLE' from a per-tool limit inside tools/call). To keep serving with per-instance counters instead, opt in explicitly (a mid-run outage then logs one warning, uses per-instance counters, and returns to Redis once it answers):
throttle.storage takes the @frontmcp/utils storage shape ({ type: 'redis', redis: { config } } or { type: 'redis', redis: { url } }), not the top-level redis shape (which takes { host, ... } or { url }).

Environment Variables

Redis Configuration

Plugin Secrets

In development, REMEMBER_SECRET is auto-generated and stored in .frontmcp/remember-secret.json. Add this file to .gitignore. In production, always set REMEMBER_SECRET explicitly to ensure consistent encryption across instances.

Health Checks

Troubleshooting

Connection Refused

  • Verify Redis is running: redis-cli ping
  • Check firewall rules
  • Verify host/port configuration
  • If startup fails, or rate-limited calls are refused, with GuardStorageUnavailableError, the throttle.storage backend is down; see When Redis is unreachable at startup

Clients keep re-initializing or tools repeat their questions

  • A 404 for a session the client just used, on a load-balanced deployment, means the instances run with different MCP_SESSION_SECRET values
  • A 2026-07-28 tool that asks its first question again after it was answered, with mcp-20260728: rejected requestState and reason: 'bad-signature' in the log, means VAULT_SECRET (or JWT_SECRET) is unset or differs between instances

Authentication Failed

  • Ensure REDIS_PASSWORD matches server config
  • Check for special characters in password (may need URL encoding)

Memory Issues

  • Set maxmemory and maxmemory-policy in Redis config
  • Monitor with redis-cli INFO memory

Docker Network Issues

When using Docker Compose, the app container should use redis as the hostname (the service name), not localhost: