Summary
commandTimeout is described as a timeout for a command that does not return a reply, but Redis.sendCommand() starts the per-command timer before deciding whether the command is writable or must be queued in offlineQueue. With a pending connection, a command can reject with Command timed out while the client is still connecting and the command has not been written to Redis.
Code path
Producer / lifecycle / feedback anchors:
lib/redis/RedisOptions.ts:16-20: the option text describes a command not returning a reply.
lib/Redis.ts:465-467: command.setTimeout(this.options.commandTimeout) is called before the writable/offline-queue decision.
lib/Redis.ts:489-520: if the stream is not writable and offline queue is enabled, the command is pushed into offlineQueue.
lib/Command.ts:375-380: the timer rejects with Command timed out.
lib/Command.ts:488-495 and lib/Command.ts:527-533: reject clears timers and rejects the promise, while only resolve marks isResolved = true.
Steps to reproduce
Validation level: dynamic reproduction plus source-control-flow validation.
Use a public Connector option whose connect() never resolves, then issue the first command with lazyConnect: true and a short commandTimeout.
const Redis = require('ioredis')
class HangingConnector {
connect () {
return new Promise(() => {})
}
disconnect () {}
check () { return true }
}
async function main () {
const redis = new Redis({
Connector: HangingConnector,
lazyConnect: true,
commandTimeout: 20,
retryStrategy: null,
})
const started = Date.now()
try {
await redis.get('offline-key')
} catch (err) {
console.log(`elapsed_ms=${Date.now() - started}`)
console.log(`status=${redis.status}`)
console.log(`error.message=${err.message}`)
} finally {
redis.disconnect()
}
}
main()
Observed output from the local repro:
elapsed_ms=22
status=connecting
error.message=Command timed out
Expected behavior
If commandTimeout means reply timeout, it should start when the command is actually written and awaiting a Redis reply. If it is intended to include offline queue wait time, the queued item should be removed/settled when it times out and the error should clearly describe queue wait timeout semantics.
Actual behavior
The timer starts before the command is writable. The user receives Command timed out while the client is still connecting and before the command has been sent.
Existing coverage
I searched current issues and PRs for commandTimeout offlineQueue, Command timed out connecting, and broader commandTimeout matches. I found adjacent issues such as #1604, #1431, and #1535, but they do not appear to cover this standalone sendCommand() timer placement before the writable/offline-queue decision.
Suggested fix
Either arm commandTimeout only after a command is written to the stream, or explicitly treat offline-queue waiting as part of the timeout contract and remove/mark the queued command when that timeout fires.
Suggested tests
- Add a standalone test with a custom
Connector whose connect() stays pending, lazyConnect: true, and a short commandTimeout.
- Assert that reply-only semantics do not produce
Command timed out before write.
- If queue-timeout semantics are intended, assert the timed-out command is removed from
offlineQueue and cannot later be flushed.
Submitted with Codex.
Summary
commandTimeoutis described as a timeout for a command that does not return a reply, butRedis.sendCommand()starts the per-command timer before deciding whether the command is writable or must be queued inofflineQueue. With a pending connection, a command can reject withCommand timed outwhile the client is stillconnectingand the command has not been written to Redis.Code path
Producer / lifecycle / feedback anchors:
lib/redis/RedisOptions.ts:16-20: the option text describes a command not returning a reply.lib/Redis.ts:465-467:command.setTimeout(this.options.commandTimeout)is called before the writable/offline-queue decision.lib/Redis.ts:489-520: if the stream is not writable and offline queue is enabled, the command is pushed intoofflineQueue.lib/Command.ts:375-380: the timer rejects withCommand timed out.lib/Command.ts:488-495andlib/Command.ts:527-533: reject clears timers and rejects the promise, while only resolve marksisResolved = true.Steps to reproduce
Validation level: dynamic reproduction plus source-control-flow validation.
Use a public
Connectoroption whoseconnect()never resolves, then issue the first command withlazyConnect: trueand a shortcommandTimeout.Observed output from the local repro:
Expected behavior
If
commandTimeoutmeans reply timeout, it should start when the command is actually written and awaiting a Redis reply. If it is intended to include offline queue wait time, the queued item should be removed/settled when it times out and the error should clearly describe queue wait timeout semantics.Actual behavior
The timer starts before the command is writable. The user receives
Command timed outwhile the client is still connecting and before the command has been sent.Existing coverage
I searched current issues and PRs for
commandTimeout offlineQueue,Command timed out connecting, and broadercommandTimeoutmatches. I found adjacent issues such as #1604, #1431, and #1535, but they do not appear to cover this standalonesendCommand()timer placement before the writable/offline-queue decision.Suggested fix
Either arm
commandTimeoutonly after a command is written to the stream, or explicitly treat offline-queue waiting as part of the timeout contract and remove/mark the queued command when that timeout fires.Suggested tests
Connectorwhoseconnect()stays pending,lazyConnect: true, and a shortcommandTimeout.Command timed outbefore write.offlineQueueand cannot later be flushed.Submitted with Codex.