go-redis/redis · error

redis: expected '*', but got line %q

Error message

redis: expected '*', but got line %q

What it means

Thrown by txPipelineReadQueued after MULTI sends QUEUED acknowledgements for every command: the next line from the server must be a RESP array ('*<n>') giving the number of replies. If the first byte is not '*' (e.g. an inline error, a push frame leaking through, or protocol desync), the pipeline transaction is considered corrupt and the error is returned.

Source

Thrown at redis.go:1889

			}
		}
	}

	// To be sure there are no buffered push notifications, we process them before reading the reply
	if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
		internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
	}
	// Parse number of replies.
	line, err := rd.ReadLine()
	if err != nil {
		if err == Nil {
			err = TxFailedErr
		}
		return err
	}

	if line[0] != proto.RespArray {
		return fmt.Errorf("redis: expected '*', but got line %q", line)
	}

	return nil
}

//------------------------------------------------------------------------------

// Client is a Redis client representing a pool of zero or more underlying connections.
// It's safe for concurrent use by multiple goroutines.
//
// Client creates and frees connections automatically; it also maintains a free pool
// of idle connections. You can control the pool size with Config.PoolSize option.
type Client struct {
	*baseClient
	cmdable

	// cscLifecycleOwner keeps the canonical Client wrapper (the one whose GC
	// cleanup owns the drainer) reachable while a WithTimeout clone can still

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Check the %q line value in the error — it tells you exactly what the server sent instead of '*'; an error string points at a server-side rejection.
  2. If a proxy is in the path, bypass it and retry against the real Redis to isolate proxy protocol bugs.
  3. Ensure the server is not in LOADING state or out-of-memory during the EXEC; both can produce non-array replies.
  4. Reproduce with redis-cli MULTI/EXEC to confirm the server emits the expected array count.
Defensive patterns

Strategy: retry

Validate before calling

// There is no caller-side input that prevents a protocol desync;
// instead validate the server is RESP-compliant before transacting.
if err := probeClient.Ping(ctx).Err(); err != nil {
    return fmt.Errorf("server not healthy for transactions: %w", err)
}

Try / catch

err := client.Watch(ctx, func(tx *redis.Tx) error { ... }, key)
if err != nil && strings.Contains(err.Error(), "expected '*'") {
    // protocol desync: reconnect and retry once; if it recurs, report server/proxy bug
}

Prevention

When it happens

Trigger: Calling a MULTI/EXEC transaction (TxPipeline, Watch transaction, or any tx-pipelined path) against a server or proxy that does not return the standard RESP array count after QUEUED, or after a connection state where a non-array reply appears mid-transaction.

Common situations: A RESP-incompatible proxy or LSW/APT redis surrogate injected an inline reply; protocol desync caused by an earlier malformed command; an upstream that injects a status/error line instead of the array count; a corrupt TCP stream or TLS middlebox; a server-side OUT-OF-MEMORY or loading reply mid-EXEC.

Related errors


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