hibiken/asynq · error
no token found for task
Error message
no token found for task %q
What it means
This error is returned by Semaphore.Release when the Redis ZREM operation against the semaphore's sorted set removes zero members, meaning no token/lease was recorded in Redis for the given taskID. The library treats releasing a task that never acquired (or already released / was evicted from) the semaphore as a caller error rather than silently succeeding, so the caller knows its bookkeeping is off.
Solutions
- Ensure Release is called exactly once per successful Acquire, guarding with a bool/sync.Once or a 'released' flag in deferred cleanup
- Verify the taskID passed to Release is byte-identical to the one returned by/used in Acquire
- Treat the error as idempotent-safe if double-release is expected in your design: log and continue instead of failing the worker
- Check for lease expiry or external cleanup of the semaphore key that removes tokens before Release runs
Example fix
// before
defer s.Release(ctx, taskID)
// after
var released bool
defer func() {
if !released {
released = true
if err := s.Release(ctx, taskID); err != nil {
log.Printf("semaphore release skipped: %v", err)
}
}
}() Defensive patterns
Strategy: try-catch
Validate before calling
// Go has no pre-call check without a round trip; optionally verify the token exists first:
n, _ := s.rc.ZScore(ctx, semaphoreKey(s.scope), taskID).Result()
if errors.Is(n, redis.Nil) { return nil /* already released */ } Try / catch
if err := sem.Release(ctx, taskID); err != nil {
if strings.Contains(err.Error(), "no token found for task") {
log.Printf("task %s already released or expired, ignoring", taskID)
return nil
}
return err
} Prevention
- Track acquired taskIDs locally and only Release ones you acquired
- Use sync.Once or a bool flag so deferred release runs at most once
- Keep the exact taskID value from Acquire; do not reformat or substitute job IDs
- Decide explicitly whether double-release should be an error or a no-op and handle consistently
When it happens
Trigger: Calling Release(taskID) with a taskID that never called Acquire, calling Release twice for the same taskID (the second ZREM deletes nothing), the token expiring or being removed from the sorted set (e.g. lease expiry/cleanup) before Release runs, or using a taskID string that differs from the one passed to Acquire (case/format mismatch).
Common situations: Worker code that releases in a deferred cleanup path which can run more than once; retries that re-release an already-released task after a failed job; passing a job ID instead of the acquire-time task ID; Redis flush/restart wiping the semaphore key between Acquire and Release.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- redis command failed
- rate.NewSemaphore: unsupported RedisConnOpt type %T
- task already exists
- task ID conflicts with another task
- queue not found
AI-assisted analysis of hibiken/asynq@d135f1439b (2026-09-07).
Data as JSON: /api/errors/d3c7b5b20a458a25.
Report an issue: GitHub.
Appendix: source
Thrown at x/rate/semaphore.go:101
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)