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

  1. Inspect the stack trace logged by internal.Logger (set redis.SetLogger to capture it) to find the panicking frame.
  2. Make the hook defensive: recover inside the hook itself, or guard nil/length assumptions before use.
  3. Check every cmd.Err() in the batch (not just one) — setCmdsErr stamps the same error on all commands that had no error yet.
  4. 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

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


AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06). Data as JSON: /data/errors/1c16e8e7da51eb4f.json. Report an issue: GitHub.