{"id":"df552659d5e2d4a5","repo":"go-redis/redis","slug":"redis-autopipeline-close-timed-out-after-s-with","errorCode":null,"errorMessage":"redis: autopipeline: Close timed out after %s with %s still in flight; they hold pooled connections until the server or the OS ends them (most often a blocking command with no timeout, or ReadTimeout disabled)","messagePattern":"redis: autopipeline: Close timed out after (.+?) with (.+?) still in flight; they hold pooled connections until the server or the OS ends them \\(most often a blocking command with no timeout, or ReadTimeout disabled\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"autopipeline.go","lineNumber":1690,"sourceCode":"\t\t\tif !batchesDone {\n\t\t\t\t// Name the precise stage: a wedged flusher and a wedged batch\n\t\t\t\t// dispatch need different operator responses.\n\t\t\t\tselect {\n\t\t\t\tcase <-flushers:\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-swept:\n\t\t\t\t\t\toutstanding = append(outstanding, \"batch dispatches\")\n\t\t\t\t\tdefault:\n\t\t\t\t\t\toutstanding = append(outstanding, \"the shutdown flush\")\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\toutstanding = append(outstanding, \"the flusher drain\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !divertedDone {\n\t\t\t\toutstanding = append(outstanding, \"diverted (blocking) commands\")\n\t\t\t}\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"redis: autopipeline: Close timed out after %s with %s still in flight; \"+\n\t\t\t\t\t\"they hold pooled connections until the server or the OS ends them \"+\n\t\t\t\t\t\"(most often a blocking command with no timeout, or ReadTimeout disabled)\",\n\t\t\t\ttimeout, strings.Join(outstanding, \" and \"))\n\t\t}\n\t}\n\treturn nil\n}\n\n// flusher is the per-shard background goroutine that flushes batches.\nfunc (s *apShard) flusher() {\n\tdefer s.ap.wg.Done()\n\tap := s.ap\n\n\tfor {\n\t\t// Wait for a command to arrive (or shutdown). The notify channel is a\n\t\t// cheap buffered wake-up; no lock is taken on the hot enqueue path.\n\t\tif s.Len() == 0 {","sourceCodeStart":1672,"sourceCodeEnd":1708,"githubUrl":"https://github.com/go-redis/redis/blob/36d97525cd8076aed67cddf54778e9ea84550929/autopipeline.go#L1672-L1708","documentation":"Returned by AutoPipeliner.Close() when its single bounded drain (autoPipelineCloseBackstop) expires before every in-flight flush, batch dispatch, and diverted blocking command finishes. Close cannot cancel in-flight dispatches once commands are accepted, so a blocking command with no timeout, or a stalled read against a dead peer with ReadTimeout disabled, has nothing to end it; the bound returns this error instead of hanging forever. The engine is already closed to new work — leaked goroutines end when the server or OS tears down the connection.","triggerScenarios":"Calling Close() while a diverted blocking command (BLPOP/XREAD with BLOCK 0) is in flight, or while a flush is stuck reading from a dead server and the client has ReadTimeout: 0 (disabled). The timer at autopipeline.go:1670 fires before batchesDone/divertedDone both complete.","commonSituations":"Long-running consumer goroutines issuing BLPOP with no timeout then shutting down the client; ReadTimeout disabled for latency-sensitive workloads combined with a network partition or a slow/dead Redis; closing the client before draining application-level consumers.","solutions":["Set a finite ReadTimeout (and/or a per-command timeout) so stalled reads cannot outlive Close.","Stop consumer goroutines and wait for them to return BEFORE calling Close(), so no blocking command is still in flight.","Issue blocking commands with an explicit finite BLOCK timeout (e.g. XREAD BLOCK 5000) instead of BLOCK 0.","On this error, treat the client as closed (no new work accepted) and exit the process or rotate the connection; do not retry Close in a tight loop."],"exampleFix":"// before\nopts := redis.Options{Addr: addr /* ReadTimeout: 0 default */}\nrdb := redis.NewClient(&opts)\n// ... a goroutine runs BLPOP with no timeout ...\nrdb.Close() // may time out\n\n// after\nopts := redis.Options{\n    Addr:        addr,\n    ReadTimeout: 5 * time.Second,\n}\n// consumer issues BLPOP with a finite timeout; cancel its context before Close\ncancel()\nconsumerWg.Wait()\nif err := rdb.Close(); err != nil {\n    log.Printf(\"close returned: %v (in-flight ops will drain when conn closes)\", err)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := rdb.Close(); err != nil {\n    // Engine is closed to new work; in-flight ops end when the conn is torn down.\n    // Do NOT loop retrying Close. Log and rotate/exit.\n    if strings.Contains(err.Error(), \"Close timed out\") {\n        log.Printf(\"redis close timed out: %v\", err)\n    } else {\n        return err\n    }\n}","preventionTips":["Always set a finite ReadTimeout on clients that issue blocking commands.","Stop consumer goroutines and wg.Wait() BEFORE calling Close().","Prefer finite BLOCK timeouts (e.g. XREAD BLOCK 5000) over BLOCK 0.","Never background-poll Close in a retry loop — on this error the engine is already closed."],"tags":["autopipeline","shutdown","blocking-command","timeout","connection-pool"],"analyzedSha":"36d97525cd8076aed67cddf54778e9ea84550929","analyzedAt":"2026-08-06T01:08:27.376Z","schemaVersion":2}