hibiken/asynq · error

rate.NewSemaphore: unsupported RedisConnOpt type %T

Error message

rate.NewSemaphore: unsupported RedisConnOpt type %T

What it means

asynq's x/rate.NewSemaphore panics when the provided RedisConnOpt does not produce a client implementinging redis.UniversalClient via MakeRedisClient(). Only RedisClient, RedisClusterClient, and FailoverClient options are supported. Any other RedisConnOpt implementation is rejected at construction time.

Solutions

  1. Use a supported option: asynq.NewRedisClientOptimizer or asynq.ParseRedisURI("redis://...") / asynq.RedisClientOpt{Addr: ...}.
  2. If using a cluster, switch to asynq.RedisClusterClientOpt.
  3. Verify rco.MakeRedisClient() returns a value implementing redis.UniversalClient before calling NewSemaphore.
  4. In tests, use a real/miniredis-backed RedisClientOpt instead of a custom mock RedisConnOpt.

Example fix

// before
type myOpt struct{}
func (myOpt) MakeRedisClient() interface{} { return someCustomClient }
sem := rate.NewSemaphore(myOpt{}, "scope", 10)
// after
rco := asynq.RedisClientOpt{Addr: "localhost:6379"}
sem := rate.NewSemaphore(rco, "my-scope", 10)
Defensive patterns

Strategy: type-guard

Validate before calling

func isSupportedOpt(rco asynq.RedisConnOpt) bool {
    _, ok := rco.MakeRedisClient().(redis.UniversalClient)
    return ok
}

Type guard

func asUniversalClient(rco asynq.RedisConnOpt) (redis.UniversalClient, bool) {
    c, ok := rco.MakeRedisClient().(redis.UniversalClient)
    return c, ok
}

Try / catch

// panics cannot be caught by try/catch in Go; guard before calling:
if c, ok := asUniversalClient(rco); ok {
    _ = rate.NewSemaphore(rco, scope, tokens)
} else {
    return fmt.Errorf("unsupported RedisConnOpt %T; use RedisClientOpt/RedisClusterClientOpt/FailoverClientOpt", rco)
}

Prevention

When it happens

Trigger: Calling rate.NewSemaphore(rco, scope, maxTokens) where rco is a custom RedisConnOpt implementation, a nil option, or a type whose MakeRedisClient() returns something other than a redis.UniversalClient (e.g. a plain redis.Client is fine, but a mock or wrapper is not).

Common situations: Passing a custom/mock RedisConnOpt in tests; using an older or third-party RedisConnOpt implementation; passing a nil or wrongly-typed option after refactoring; assuming any asynq.RedisConnOpt works with the x/rate package.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

Thrown at x/rate/semaphore.go:19

// Package rate contains rate limiting strategies for asynq.Handler(s).
package rate

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/hibiken/asynq"
	asynqcontext "github.com/hibiken/asynq/internal/context"
	"github.com/redis/go-redis/v9"
)

// NewSemaphore creates a counting Semaphore for the given scope with the given number of tokens.
func NewSemaphore(rco asynq.RedisConnOpt, scope string, maxTokens int) *Semaphore {
	rc, ok := rco.MakeRedisClient().(redis.UniversalClient)
	if !ok {
		panic(fmt.Sprintf("rate.NewSemaphore: unsupported RedisConnOpt type %T", rco))
	}

	if maxTokens < 1 {
		panic("rate.NewSemaphore: maxTokens cannot be less than 1")
	}

	if len(strings.TrimSpace(scope)) == 0 {
		panic("rate.NewSemaphore: scope should not be empty")
	}

	return &Semaphore{
		rc:        rc,
		scope:     scope,
		maxTokens: maxTokens,
	}
}

// Semaphore is a distributed counting semaphore which can be used to set maxTokens across multiple asynq servers.

View on GitHub (pinned to d135f1439b)