---
title: Valkey
description: The Redis store on node-redis, for Valkey and Redis servers that need TLS settings, rotating credentials, or reconnect control.
---

```ts server/orders.ts
import { createServer } from 'experimental-a2/server'
import { valkey } from 'experimental-a2/store-valkey'
import { orders } from '@/contracts'

export const ordersServer = createServer({
  contract: orders,
  store: valkey({ url: process.env.VALKEY_URL }),
  consumers: [
    /* ... */
  ],
})
```

```package-install
npm i redis
```

Stores sessions exactly like [Redis](/stores/redis), on
[node-redis](https://github.com/redis/node-redis) connections instead of
`ioredis`. Use it with Valkey or Redis when a URL isn't enough: managed
servers such as ElastiCache that authenticate with short-lived tokens, custom
TLS settings, or your own reconnect and timeout policy. It needs a single
server or a non-cluster replication group: it holds one non-cluster
connection for commands and one for pub/sub.

## Options

| Option | |
| --- | --- |
| `url` | a `redis://` or `rediss://` URL |
| `clientOptions` | node-redis `createClient` options for both connections, such as `socket`, `credentialsProvider`, `commandOptions`, `disableOfflineQueue` |
| `keyPrefix` | isolates several apps on one server. Default `'a2'` |
| `clock`, `ids` | test seams for timestamps and generated event ids |

Pass `url`, `clientOptions`, or both; `url` wins over `clientOptions.url`.
A2 always connects with `RESP: 2`, so `clientOptions` has no `RESP`, nor the
options that need RESP3 (`clientSideCache`, `emitInvalidate`,
`maintNotifications`).

## Rotating credentials

An async credentials provider runs each time a connection authenticates,
including every reconnect, so each one gets a fresh token:

```ts server/store.ts
import { valkey } from 'experimental-a2/store-valkey'

export const store = valkey({
  clientOptions: {
    socket: { host: 'my-cache.example.com', port: 6379, tls: true },
    credentialsProvider: {
      type: 'async-credentials-provider',
      credentials: async () => {
        // your token source, such as an ElastiCache IAM auth token:
        // const password = await createAuthToken()
        const password = 'token'
        return { username: 'a2', password }
      },
    },
  },
})
```

## Connections

A2 creates two clients from your options: one for commands, on the first
store call, and one subscriber, on the first live stream or presence
subscription. Every live stream and presence subscription in the process
shares that subscriber. `close()` destroys both.

- A new client is used once it first connects: calls wait for it, each for at
  most `commandOptions.timeout` (five seconds without one), so a cold start
  works with `disableOfflineQueue`.
- While a client reconnects, node-redis's own options decide what happens to
  commands: they wait in its offline queue, bounded by
  `commandOptions.timeout`, or fail at once with `disableOfflineQueue`. A2
  applies the same rules to a new live stream's subscription, which node-redis
  would otherwise queue without a bound.
- A reconnect follows your `socket.reconnectStrategy`. When the subscriber
  reconnects, every live stream reads what it missed right away, without
  waiting for the safety re-read.
- When a client's reconnect strategy gives up, A2 replaces it: the command
  client on the next store call, the subscriber on the next new live stream or
  presence subscription. Until then, live streams still deliver within the
  ten-second safety re-read.
- A2 listens for each client's `'error'` events, so a dropped socket never
  crashes the process. Failures surface as `STORE_UNAVAILABLE` from the call
  that hit them.

The upgrade rules, key layout, and per-command costs are the same as
[Redis](/stores/redis).
