hibiken/asynq · error

redis command failed

Error message

redis command failed: %w

What it means

Semaphore.Release removes the caller's token from the Redis sorted set with ZRem. If the Redis command itself fails (connection error, timeout, read-only replica, cluster error), the error is wrapped as "redis command failed" and returned.

Solutions

  1. Check Redis availability (redis-cli ping) and fix connection settings in RedisClientOpt.
  2. Unwrap the %w cause to identify connection-refused vs timeout vs auth errors.
  3. Add retry with backoff for Release on transient network errors, since failing to release stalls the semaphore slot.
  4. Ensure the Semaphore and the asynq server use the same reachable Redis instance/database.

Example fix

// before
sem := NewSemaphore(rdb, "scope", 1)
// rdb points at 127.0.0.1:6399 (down)
// after
sem := NewSemaphore(rdb, "scope", 1)
if err := sem.Release(ctx); err != nil {
    log.Printf("release failed: %v", err) // inspect wrapped redis cause
}
Defensive patterns

Strategy: retry

Validate before calling

if err := rdb.Ping(ctx).Err(); err != nil {
    log.Printf("redis unavailable: %v", err)
}

Try / catch

if err := sem.Release(ctx); err != nil {
    // retry a few times before giving up so the slot isn't leaked
    for i := 0; i < 3; i++ {
        time.Sleep(100*time.Millisecond)
        if err = sem.Release(ctx); err == nil { break }
    }
    log.Printf("release failed after retries: %v", err)
}

Prevention

When it happens

Trigger: Calling sem.Release(ctx) while Redis is unreachable, the connection pool is exhausted, the key is in a wrong cluster slot, or Redis returns any command-level error.

Common situations: Redis restart or failover mid-processing; network blips between app and Redis; connecting the Semaphore to a different/replica Redis instance than intended; AUTH required but not configured.

Related errors


AI-assisted analysis of hibiken/asynq@d135f1439b (2026-09-07). Data as JSON: /api/errors/78be52bf40abf735. Report an issue: GitHub.

Appendix: source

Thrown at x/rate/semaphore.go:97

	return acquireCmd.Run(ctx, s.rc,
		[]string{semaphoreKey(s.scope)},
		s.maxTokens,
		time.Now().Unix(),
		d.Unix(),
		taskID,
	).Bool()
}

// Release will release the token on the counting semaphore.
func (s *Semaphore) Release(ctx context.Context) error {
	taskID, ok := asynqcontext.GetTaskID(ctx)
	if !ok {
		return fmt.Errorf("provided context is missing task ID value")
	}

	n, err := s.rc.ZRem(ctx, semaphoreKey(s.scope), taskID).Result()
	if err != nil {
		return fmt.Errorf("redis command failed: %w", err)
	}

	if n == 0 {
		return fmt.Errorf("no token found for task %q", taskID)
	}

	return nil
}

// Close closes the connection to redis.
func (s *Semaphore) Close() error {
	return s.rc.Close()
}

func semaphoreKey(scope string) string {
	return "asynq:sema:" + scope
}

View on GitHub (pinned to d135f1439b)