hibiken/asynq · error

rate.NewSemaphore: scope should not be empty

Error message

rate.NewSemaphore: scope should not be empty

What it means

rate.NewSemaphore panics when the scope argument is empty or consists only of whitespace. The scope keys the semaphore's token set in Redis, so an empty scope would produce ambiguous, colliding keys and is rejected up front.

Solutions

  1. Pass a non-empty, meaningful scope string (e.g. "myapp:api").
  2. Trim and validate the scope before calling NewSemaphore; return a clear error if blank.
  3. If scope is dynamic, check its source (flag, env var, tenant context) is populated first.
  4. Include an application prefix in the scope to avoid cross-app key collisions in shared Redis.

Example fix

// before
scope := os.Getenv("RATE_SCOPE") // ""
sem := rate.NewSemaphore(rco, scope, 10)
// after
scope := os.Getenv("RATE_SCOPE")
if strings.TrimSpace(scope) == "" { log.Fatal("RATE_SCOPE must be set") }
sem := rate.NewSemaphore(rco, scope, 10)
Defensive patterns

Strategy: validation

Validate before calling

func validateScope(scope string) error {
    if strings.TrimSpace(scope) == "" {
        return errors.New("scope must be a non-empty string")
    }
    return nil
}

Try / catch

// Go panics are not catchable via try/catch; validate first:
if err := validateScope(scope); err != nil {
    return nil, err
}
sem := rate.NewSemaphore(rco, scope, tokens)

Prevention

When it happens

Trigger: Calling rate.NewSemaphore(rco, "", n) or rate.NewSemaphore(rco, " ", n) — e.g. when scope is built from a template, tenant ID, or flag that is unset.

Common situations: Missing CLI flag or env var for the scope/tenant name; string concatenation producing an empty value; using a variable before it is initialized.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at x/rate/semaphore.go:27

	"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.
type Semaphore struct {
	rc        redis.UniversalClient
	maxTokens int
	scope     string
}

// KEYS[1] -> asynq:sema:<scope>
// ARGV[1] -> max concurrency

View on GitHub (pinned to d135f1439b)