How to Fix Node Redis ClientClosedError: The client is closed

Quick Diagnosis

  • ✓ Did you recently upgrade the redis npm package to version 4 or newer?
  • ✓ Does your application throw ClientClosedError: The client is closed when executing cache operations?
  • ✓ Is your code invoking commands immediately after calling createClient() without calling .connect()?

Environment

The Redis connection driver manages TCP socket lifecycle events server-side, executing across configurations including Express API Server contexts, Next.js dynamic Route Handlers, Standalone Node.js scripts, and Serverless Vercel Functions runtimes.

Client Creation Call Connection Command Query Command Call Client Execution Outcome
const client = createClient() Omitted (Direct command call) await client.set('key', 'value') Failed (ClientClosedError: The client is closed)
const client = createClient() client.connect() (Unawaited) await client.set('key', 'value') Hangs indefinitely (Queues commands in memory)
const client = createClient() await client.connect() await client.set('key', 'value') Success (Resolves command and updates cache)

Minimal Repro

Under the architecture of Node Redis SDK v4, the driver underwent a complete structural rewrite to support native promises, improve TypeScript bindings, and decouple client setup from background TCP socket initialization. In legacy v3, creating a client instance initiated an automatic background socket connection. In v4, calling the createClient factory constructor builds the manager context object in memory but leaves the connection status closed (client.isOpen is false). If your codebase attempts to dispatch read or write commands (such as set or get) before executing the connection, the driver checks the closed socket state and rejects the execution. The library aborts the request, throwing a ClientClosedError stating The client is closed. Applying the explicit connect method and awaiting the connection lifecycle ensures socket readiness.

const { createClient } = require('redis');

// In SDK v4, this does NOT initiate a socket connection
const client = createClient();

client.on('error', (err) => console.log('Redis Client Error', err));

async function runRepro() {
  // CRASH: Client is closed, command fails immediately
  await client.set('mykey', 'myvalue');
  const val = await client.get('mykey');
  console.log(val);
}

runRepro();
ClientClosedError: The client is closed
    at Client.set (node_modules/@redis/client/dist/lib/client/index.js:452:19)
    at runRepro (repro.js:10:16)

Resolution

When resolving Node Redis connection exceptions, developers can choose between two main structural options depending on their legacy code layout.

If you build modern applications using Node Redis SDK v4, calling and awaiting .connect() immediately after client initialization is applicable. This configuration guarantees socket readiness before queries dispatch.

  1. Import createClient from the package.
  2. Register an 'error' event listener to prevent unhandled background re-connection crash events.
  3. Execute and await client.connect().
import { createClient } from 'redis';

// Initialize manager context
const client = createClient({
  url: process.env.REDIS_URL
});

// Bind error event listeners
client.on('error', (err) => console.error('[Redis Client Exception]:', err));

// Explicitly connect and await TCP handshake
await client.connect();

export async function fetchCacheValue(key) {
  // Client is open, commands resolve successfully
  return await client.get(key);
}

Option B: Configure legacyMode for v3 Backwards Compatibility

If you deploy large applications containing legacy v3 callbacks that you cannot refactor immediately, configuring legacyMode: true inside your client properties is applicable.

  1. Initialize your client specifying legacyMode: true.
  2. Explicitly call client.connect().
  3. Wrap your queries in legacy callback wrappers or use .v4 namespaces for modern queries.
const { createClient } = require('redis');

// Set legacy configuration mode
const client = createClient({
  url: process.env.REDIS_URL,
  legacyMode: true
});

client.on('error', (err) => console.error('Redis Error', err));

// Connection call is still required
client.connect();

// Run legacy v3 format query
client.set('key', 'value', (err, reply) => {
  console.log(reply); // OK
});

When This Fix Won’t Work

If you call client.connect() but omit the await operator, commands dispatched immediately after will be queued in memory, potentially causing the process to hang if connection retries time out.

Operational Runbook

Case 1: Client Setup

  1. Instantiate clients using the createClient factory.
  2. Bind 'error' and 'connect' listeners immediately.
  3. Call await client.connect().

Case 2: Serverless Lifecycles

  1. In serverless environments, check client.isOpen before dispatching queries.
  2. Reuse warm container instances where possible.

Rollback Strategy

To roll back this change, replace explicit client.connect() function calls in the startup file with legacy direct queries, activate the legacyMode: true parameters inside client configurations, and downgrade your package dependencies to Node Redis v3.x.


Verification

  • Application cache commands complete successfully without throwing ClientClosedError exceptions.
  • Server initialization logs display successful connection handshakes with the Redis host.
  • Integration tests verify that data reads and writes execute with zero socket hang timeouts.

Error Trigger Point Lifecycle

Call createClient factory ➔ Attach error event listener ➔ Call client.connect() [ERROR OCCURS HERE] ➔ Await TCP socket handshake ➔ Execute redis commands ➔ Close client socket

References