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

  1. Pass a positive integer for maxTokens (e.g. 10).
  2. If maxTokens comes from config, default it to a sane positive value when unset (e.g. if v <= 0 { v = defaultMaxTokens }).
  3. Validate the value before calling NewSemaphore and fail with a descriptive error.
  4. 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

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


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     string

View on GitHub (pinned to d135f1439b)