node-redis icon indicating copy to clipboard operation
node-redis copied to clipboard

`RedisClientType` type is slow to typecheck (TS performance issue)

Open JulienZD opened this issue 7 months ago • 15 comments

Description

We just upgraded redis from 4.7.0 to 5.1.0. In our CI we have a job to verify that all types are still correct (using tsc --noEmit). Since upgrading redis this job now runs out of memory:

CI log

> tsc --noEmit -p tsconfig.test.json
<--- Last few GCs --->
[47:0x7d3f4fbaf000]    79561 ms: Mark-Compact 2043.8 (2085.9) -> 2042.7 (2086.9) MB, pooled: 0 MB, 2499.84 / 0.00 ms  (average mu = 0.093, current mu = 0.008) allocation failure; scavenge might not succeed
[47:0x7d3f4fbaf000]    82274 ms: Mark-Compact 2044.7 (2086.9) -> 2043.6 (2087.9) MB, pooled: 0 MB, 2692.69 / 0.00 ms  (average mu = 0.049, current mu = 0.007) allocation failure; scavenge might not succeed
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----
 ELIFECYCLE  Command failed.
Aborted (core dumped)

I can reproduce this locally using NODE_OPTIONS="--max-old-space-size=2048" pnpm tsc --noEmit -p tsconfig.test.json (this does also include our application code, so the max size is probably different for a small reproduction).

I analyzed the output on my local machine (Macbook M3 Pro, 36GB RAM) using @typescript/analyze-trace, which gave the following output:

Hot Spots
└─ Check file [35m/project/test/test-helpers/[36mmocks.ts[39m[35m[39m (2747ms)
   └─ Check expression from (line 21, char 5) to (line 23, char 7) (2734ms)
      └─ Check expression from (line 21, char 18) to (line 23, char 7) (2717ms)
         └─ Determine variance of type 335076 (2638ms)
            └─ Compare types 689415 and 689406 (1366ms)
               └─ Compare types 689415 and 689398 (1366ms)
                  └─ Compare types 689407 and 689398 (1330ms)
                     ├─ {"id":689407,"kind":"GenericTypeAlias","name":"WithCommands","aliasTypeArguments":[335026,335036,335037,335038,47,335040],"location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":9,"char":1}}
                     │  ├─ {"id":335026,"kind":"TypeParameter","name":"REPLIES","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":41}}
                     │  ├─ {"id":335036,"kind":"TypeParameter","name":"M","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":69}}
                     │  ├─ {"id":335037,"kind":"TypeParameter","name":"F","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":93}}
                     │  ├─ {"id":335038,"kind":"TypeParameter","name":"S","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":119}}
                     │  ├─ {"id":47,"kind":"TypeParameter"}
                     │  └─ {"id":335040,"kind":"TypeParameter","name":"TYPE_MAPPING","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":170}}
                     └─ {"id":689398,"kind":"GenericTypeAlias","name":"WithCommands","aliasTypeArguments":[335026,335036,335037,335038,46,335040],"location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":9,"char":1}}
                        ├─ {"id":335026,"kind":"TypeParameter","name":"REPLIES","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":41}}
                        ├─ {"id":335036,"kind":"TypeParameter","name":"M","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":69}}
                        ├─ {"id":335037,"kind":"TypeParameter","name":"F","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":93}}
                        ├─ {"id":335038,"kind":"TypeParameter","name":"S","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":119}}
                        ├─ {"id":46,"kind":"TypeParameter"}
                        └─ {"id":335040,"kind":"TypeParameter","name":"TYPE_MAPPING","location":{"path":"[35m/project/node_modules/[36m.pnpm[39m[35m/@[email protected]/node_modules/[36m@redis/client[39m[35m/dist/lib/client/multi-command.d.ts[39m","line":25,"char":170}}

This led me to a file used in our tests: Affected code (unrelated code omitted for brevity)

// test/test-helpers/mocks.ts
import { createClient, type RedisClientType } from 'redis';

export class RedisTestContainer {
  private redis: RedisClientType | undefined;
  
  public async init() {
    this.redis = createClient({ /* config */ });

    await this.redis.connect();
    
    return this.redis;
  }

  public async stop() {
    await this.redis?.stop();
  }
}

It seems that type checking RedisClientType is taking 2.7s locally, most likely taking up a bunch of memory as it does so. From the output it seems the culprit is somewhere in multi-command.d.ts.

Replacing RedisClientType with { connect: () => Promise<unknown>; quit: () => Promise<unknown> } fixes the issue for us, since those are the only two methods we use anyway.

This fixes the issue for us, as running NODE_OPTIONS="--max-old-space-size=2048" pnpm tsc --noEmit -p tsconfig.test.json now finishes without errors. Our CI also no longer runs out of memory.

Node.js Version

22.14.0

Redis Server Version

Node Redis Version

5.1.0

Platform

MacOS, Linux

Logs


JulienZD avatar May 27 '25 07:05 JulienZD

@JulienZD thanks for flagging this out. Im not sure if we can easily fix this, but I will investigate

nkaradzhov avatar May 27 '25 08:05 nkaradzhov

Just an additional anecdotal data point, but I had to revert an upgrade from v4 to v5 in one of our projects as typing in any redis related file became hardly usable with 30s+ of wait between typing something and the typechecking updating in VSCode

vbfox avatar Jun 11 '25 07:06 vbfox

I have the same issue

NguyenAnhTuan1912 avatar Jun 16 '25 09:06 NguyenAnhTuan1912

This should be considered a critical bug. I've just wasted an afternoon diagnosing this problem and then stubbing out RedisClientType with a type-safe EventEmitter replacement:

type RedisClientOn<EventDict extends Record<string | symbol, unknown[]>> = <
  ClientEvent extends keyof EventDict
>(
  event: ClientEvent,
  listener: (...args: EventDict[ClientEvent]) => void
) => unknown;

type RedisClientTypeMinimal = {
  connect: () => Promise<unknown>;
  disconnect: () => Promise<void>;
  on: RedisClientOn<{
    ready: [];
    error: [Error];
  }>;
  hGetAll: (key: string) => Promise<{ [x: string]: string }>;
};

This was tedious, to say the least. But if I try to use the actual type then I lose Intellisense and linting (at least I lose them from the point of view of usability).

daggilli avatar Jun 19 '25 23:06 daggilli

Same here, almost unusable with typescript. I will try to investigate a bit.

656d696c65 avatar Jun 20 '25 15:06 656d696c65

Seeing the same thing here which is making intellisense utterly unusable. Looking at some traces, it's taking typescript 9 seconds to check the variance of RedisClientMultiCommandType

edrose avatar Jun 25 '25 15:06 edrose

Tried migrating from ioredis to node-redis, but this issue makes the project nearly unusable. It slows down the IDE, causes freezes, and hurts overall productivity—even on a MacBook M4 Pro, where performance shouldn’t be a problem.

I’d strongly recommend raising the priority of this issue, as it’s currently a major blocker for adopting node-redis.

CSenshi avatar Jul 10 '25 17:07 CSenshi

I’ve run some benchmarks to measure the impact of the current node-redis type definitions on TypeScript build performance.

1 - With redis types

When using these types:

import { createCluster, createClient, createSentinel } from 'redis';

export type RedisClient = ReturnType<typeof createClient>;
export type RedisCluster = ReturnType<typeof createCluster>;
export type RedisSentinel = ReturnType<typeof createSentinel>;

Build times for my NestJS project are:

pnpm nx build client  
Compiling TypeScript files for project "client"...  
Done compiling TypeScript files for project "client".  

Build time: **~10.5s**  

2 - Without redis types

When replacing the above with any:

export type RedisClient = any;
export type RedisCluster = any;
export type RedisSentinel = any;

Build times drop significantly:

pnpm nx build client  
Compiling TypeScript files for project "client"...  
Done compiling TypeScript files for project "client".  

Build time: **~2.3s**  

The difference is ~8 seconds slower per build with the official types.

CSenshi avatar Jul 21 '25 16:07 CSenshi

We are experiencing the same issue, is there any fix being worked on?

tomeriksson-ikea avatar Aug 05 '25 17:08 tomeriksson-ikea

We were in the process of adopting this library for a new project, but due to this issue, we switched to ioredis.

unknownpgr avatar Aug 07 '25 01:08 unknownpgr

Hi all, really sorry about this! We are working to resolve this. Its complicated and will most likely result in a breaking change.

nkaradzhov avatar Aug 07 '25 09:08 nkaradzhov

I have been using it in my personal project, but it took my vs code intellisense a whole cpu throttling to typing and liniting with typescript.

AnkitBorude avatar Aug 12 '25 13:08 AnkitBorude

Hi everyone, thanks to @PavelPashov, we now have a mitigation for this issue. It reduces time/memory for compilation around x4 on my machine. We have put that fix in the beta for now. Is it possible someone tries if this fixes your issues? you can install the beta like this npm i redis@beta

nkaradzhov avatar Oct 02 '25 10:10 nkaradzhov

Just checked it on our codebase with [email protected] (Node.js version 22.16.0): running NODE_OPTIONS="--max-old-space-size=2048" pnpm tsc --noEmit -p tsconfig.test.json now successfully completes!

One thing to note is that adding --generateTrace ./ts-traces to the command causes it to run out of memory again. I don't think that's a huge issue, as you normally don't have to generate traces with a limited amount of memory. (generating traces does succeed with ~3GB of memory)

JulienZD avatar Oct 02 '25 11:10 JulienZD

I have small OSS project nestjs-redis and changed it there

Here are the results of compilation of libraries

Command:

pnpm nx run-many -t build 

Before:

 NX    Completed Running 6 build tasks (13.5s)                                                                                                               
  ✔    socket.io-adapter:build      -       3.6s
  ✔    throttler-storage:build      -      813ms
  ✔    health-indicator:build       -       2.6s
  ✔    lock:build                   -      999ms
  ✔    client:build                 -      10.0s
  ✔    kit:build                    -       9.9s

After:

 NX    Completed Running 6 build tasks (9.6s)                                                                                                   
  ✔    socket.io-adapter:build      -       1.3s
  ✔    throttler-storage:build      -      824ms
  ✔    health-indicator:build       -       2.5s
  ✔    lock:build                   -      797ms
  ✔    client:build                 -       7.3s
  ✔    kit:build                    -       7.1s

There definitely are some improvements but wherever I call createClient it needs more time (like in client library)

CSenshi avatar Oct 02 '25 12:10 CSenshi