Skip to content

redis() silently falls back to a default localhost connection when the passed ioredis instance is a different copy #20

Description

@irshadahmad21

Hey! 👋 Following up with another one we hit in production.

redis() decides whether it was handed a live connection or a config object with instanceof:

export function redis(config?: RedisConfig) {
  return () => {
    if (config instanceof Redis) {
      return new RedisAdapter(config, false)
    }
    const options: RedisOptions = { host: 'localhost', port: 6379, keyPrefix: 'boringnode::queue::', db: 0, ...config }
    const connection = new Redis(options)
    return new RedisAdapter(connection, true)
  }
}

ioredis is an optional peer dependency, so under pnpm the copy @boringnode/queue resolves is routinely not the copy the caller instantiated from. instanceof is then false for a perfectly valid connection, and the else-branch takes over. Spreading a Redis instance contributes no host/port/db/password, so the passed connection's settings are all silently discarded and the queue connects to localhost:6379, db 0, unauthenticated.

This hits @adonisjs/queue directly — its documented contract is "the Redis adapter uses your @adonisjs/redis connection", and it implements that as redisAdapter(connection.ioConnection).

What it looks like in practice

Our test env sets REDIS_DB=1 to isolate itself from the dev queue. The Redis service honoured it; the queue did not. Test runs enqueued real jobs into the dev queue on db 0, the dev worker picked them up after the test DB had been torn down, and we got a flood of "row no longer exists" job failures. CLIENT LIST showed the split plainly — lib-ver=5.11.1 (the app's connection) on db 1, lib-ver=5.9.1 (boringnode's own) on db 0.

The worse case is production: a deployment configuring REDIS_HOST/REDIS_PASSWORD gets a queue quietly pointed at localhost with no auth, rather than an error.

Repro

pnpm workspace, @adonisjs/queue + @adonisjs/redis, config/redis.ts with db: 1, config/queue.ts with drivers.redis({ connectionName: 'main' }). Dispatch any job → keys land in db 0. Confirmed by module resolution:

@adonisjs/redis      → ioredis@5.11.1/built/index.js
@boringnode/queue    → ioredis@5.9.1/built/index.js     // different class, instanceof === false

Suggested fix

Duck-type instead of instanceof — it can't false-negative across package copies:

if (config && typeof (config as any).defineCommand === 'function') {
  return new RedisAdapter(config as Redis, false)
}

Failing that, please make the fallback throw on an unrecognised object rather than silently substituting defaults — the silence is what made this expensive to find.

Versions

Reproduced on both @boringnode/queue@0.5.2 (via @adonisjs/queue@0.6.1) and @boringnode/queue@0.6.0 (via @adonisjs/queue@0.6.2) — same result, jobs land in db 0 either way. @adonisjs/redis@10.0.0, pnpm 11.15, Node 24. The check is unchanged on main.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions