hibiken/asynq · error
rate.NewSemaphore: maxTokens cannot be less than 1
Error message
rate.NewSemaphore: maxTokens cannot be less than 1
What it means
rate.NewSemaphore panics when maxTokens is less than 1, since a counting semaphore must grant at least one token to be meaningful. This is an eager argument-validation panic raised immediately at construction, before any Redis calls.
Solutions
- Pass a positive integer for maxTokens (e.g. 10).
- If maxTokens comes from config, default it to a sane positive value when unset (e.g. if v <= 0 { v = defaultMaxTokens }).
- Validate the value before calling NewSemaphore and fail with a descriptive error.
- Check arithmetic that computes maxTokens (division, multipliers) for zero/negative results.
Example fix
// before
sem := rate.NewSemaphore(rco, "api", cfg.MaxTokens) // MaxTokens = 0
// after
if cfg.MaxTokens < 1 { cfg.MaxTokens = 10 }
sem := rate.NewSemaphore(rco, "api", cfg.MaxTokens) Defensive patterns
Strategy: validation
Validate before calling
func validateMaxTokens(n int) error {
if n < 1 {
return fmt.Errorf("maxTokens must be >= 1, got %d", n)
}
return nil
} Try / catch
// Go panics are not catchable via try/catch; recover at process boundary if desired:
defer func() {
if r := recover(); r != nil {
log.Fatalf("rate.NewSemaphore: %v", r)
}
}() Prevention
- Default maxTokens to a positive value when config is unset.
- Check computed values (divisions, multipliers) for zero/negative results before use.
- Fail fast at config-load time with a clear message instead of at construction.
- Add a test covering the zero/default config path.
When it happens
Trigger: Calling rate.NewSemaphore(rco, scope, 0) or rate.NewSemaphore(rco, scope, -1) — typically when maxTokens is computed from config, a multiplier, or a variable that defaults to zero.
Common situations: maxTokens sourced from an unset env var or config field defaulting to 0; integer division truncation yielding 0; copy-pasted constructor calls with placeholder values.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- rate.NewSemaphore: scope should not be empty
- asynq: invalid pattern
- rate.NewSemaphore: unsupported RedisConnOpt type %T
- provided context must have a deadline
- provided context is missing task ID value
AI-assisted analysis of hibiken/asynq@d135f1439b (2026-09-07).
Data as JSON: /api/errors/fe84d41ddc32e2bf.
Report an issue: GitHub.
Appendix: source
Thrown at x/rate/semaphore.go:23
"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.
type Semaphore struct {
rc redis.UniversalClient
maxTokens int
scope stringView on GitHub (pinned to d135f1439b)