go-redis/redis · error
redis: autopipeline: panic during dispatch: %v
Error message
redis: autopipeline: panic during dispatch: %v
What it means
Produced when a panic occurs inside a dispatch goroutine of the AutoPipeliner (a user hook such as ProcessHook/DialHook, or a command encoder inside Process/Exec). Dispatch goroutines have no caller frame to recover the panic, so an unrecovered panic would crash the whole process on behalf of one bad command. recoverDispatchPanic (autopipeline.go:2175) catches it, stamps the error on every command in the affected batch(es) via setCmdsErr, and logs the stack via internal.Logger so the program keeps running.
Source
Thrown at autopipeline.go:2180
}
return append(runs, cmds[start:])
}
// recoverDispatchPanic converts a panic on a dispatch goroutine (a hook or
// command-encoder panic inside Process/Exec) into per-command errors instead
// of crashing the process. On a plain client the same panic unwinds into the
// CALLER, who can recover; the engine's dispatch goroutines have no caller,
// so an unrecovered panic here would kill the whole program on behalf of one
// bad command. Registered LAST at each dispatch site so it runs FIRST on
// unwind (LIFO) — the errors are stamped before the deferred batch closes
// wake the waiters. setCmdsErr fills only commands without an error, so
// exec-recorded outcomes for commands that finished are preserved.
func recoverDispatchPanic(cmds ...[]Cmder) {
r := recover()
if r == nil {
return
}
err := fmt.Errorf("redis: autopipeline: panic during dispatch: %v", r)
for _, batch := range cmds {
setCmdsErr(batch, err)
}
internal.Logger.Printf(context.Background(), "autopipeline: recovered dispatch panic: %v\n%s", r, debug.Stack())
}
// flushBatchSlice takes the shard's currently-queued commands as one batch,
// swaps in a fresh batch for subsequent enqueues, and dispatches the taken
// batch. Completion is signalled by closing the batch's done channel once
// (waking every waiter in a single operation) rather than one channel send
// per command.
func (s *apShard) flushBatchSlice() {
ap := s.ap
// Drain every stripe into one combined batch and roll fresh queues for the
// commands enqueued after this point. Striped enqueue spreads the hot
// mutex; one merged flush keeps the pipeline deep. accumulateBatch already
// bounds the total to roughly MaxBatchSize before we get here.View on GitHub (pinned to 36d97525cd)
Solutions
- Inspect the stack trace logged by internal.Logger (set redis.SetLogger to capture it) to find the panicking frame.
- Make the hook defensive: recover inside the hook itself, or guard nil/length assumptions before use.
- Check every cmd.Err() in the batch (not just one) — setCmdsErr stamps the same error on all commands that had no error yet.
- Reproduce with a minimal hook in a test to confirm the fix.
Example fix
// before — hook panics on nil tag
rdb.AddHook(redis.ProcessHook(func(ctx context.Context, cmd redis.Cmder, next redis.ProcessHook) error {
tag := ctx.Value("tag").(string) // panic if tag missing
fmt.Println(tag)
return next(ctx, cmd)
}))
// after — guard the assertion
rdb.AddHook(redis.ProcessHook(func(ctx context.Context, cmd redis.Cmder, next redis.ProcessHook) error {
if v, ok := ctx.Value("tag").(string); ok {
fmt.Println(v)
}
return next(ctx, cmd)
})) Defensive patterns
Strategy: try-catch
Try / catch
// Every command in the batch may carry the recovered-panic error,
// not just the first. Iterate all commands.
for _, c := range cmds {
if err := c.Err(); err != nil && strings.Contains(err.Error(), "panic during dispatch") {
// surface to monitoring; the panic stack was logged via internal.Logger
}
} Prevention
- Set redis.SetLogger so recovered-panic stacks are captured.
- Recover inside your own hooks so a bug does not poison a whole batch.
- Guard nil/length assumptions in hooks; never assume context values are present.
- Write a unit test per hook that exercises it in isolation.
When it happens
Trigger: Registering a ProcessHook that dereferences a nil pointer or indexes out of range; a custom command-encoder path that panics; a hook that closes over a resource freed concurrently. The panic is recovered on the flusher/dispatch goroutine and surfaces as cmd.Err() on the commands in that batch.
Common situations: Buggy instrumentation hook (OTel/metrics); a hook that assumes a non-nil context value the caller forgot to set; a hook upgraded across a version that changed the Cmder shape; concurrent use of a shared unsafe resource from a hook.
Related errors
- redis: AutoPipelineOptions.NumShards=%d requires Unordered:t
- redis: connection pool exhausted
- redis: connection pool timeout
- redis: AutoPipeline is not supported by Ring
- redis: AutoPipelineOptions.MaxConcurrentBatches=%d requires
AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06).
Data as JSON: /data/errors/1c16e8e7da51eb4f.json.
Report an issue: GitHub.