hibiken/asynq · warning
skip retry for the task
Error message
skip retry for the task
What it means
SkipRetry is a sentinel returned by a task Handler to tell asynq the task should NOT be retried. In processor.handleFailedMessage, when errors.Is(err, SkipRetry) matches (or retries are exhausted), the processor archives the task instead of re-scheduling it, so the failure is final.
Solutions
- Ensure the handler returns SkipRetry wrapped with %w so errors.Is detects it (or return the sentinel unwrapped).
- If you want the task retained for inspection, know that it goes to the archive — retrieve it via Inspector.ListArchivedTasks rather than expecting it in pending.
- For failures that are transient, return a plain error instead of SkipRetry so asynq's retry policy applies.
- Implement an ErrorHandler (via asynq.Config ErrorHandler) to log/track tasks that hit SkipRetry.
Example fix
// before
if resp.StatusCode >= 500 {
return fmt.Errorf("upstream failed: %v", asynq.SkipRetry) // wrong wrapping; retries anyway
}
// after
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return fmt.Errorf("permanent client error %d: %w", resp.StatusCode, asynq.SkipRetry) // archived, no retry
} Defensive patterns
Strategy: try-catch
Validate before calling
// validate input before returning errors in the handler
if !json.Valid(task.Payload()) { return fmt.Errorf("invalid payload: %w", asynq.SkipRetry) } Type guard
func isSkipRetry(err error) bool { return errors.Is(err, asynq.SkipRetry) } Try / catch
if err := handler.ProcessTask(ctx, task); err != nil {
if errors.Is(err, asynq.SkipRetry) {
log.Printf("task %s archived (no retry): %v", task.ID(), err)
}
return err
} Prevention
- Wrap sentinels with %w, never %v, so errors.Is works.
- Return SkipRetry only for permanent errors that retries cannot fix.
- Inspect the archive (Inspector.ListArchivedTasks) when tasks vanish from pending.
- Register an ErrorHandler to monitor archived tasks in production.
When it happens
Trigger: A Handler.ProcessTask implementation returns SkipRetry (optionally wrapped, e.g. fmt.Errorf("...: %w", SkipRetry)) after a failure that retrying cannot fix, and the processor's handleFailedMessage then archives the task.
Common situations: Handlers encountering permanent/input errors (malformed payload, 4xx upstream responses, validation failures) where retries would only burn cycles; developers who return it but then can't find the task in the queue and don't realize it landed in the archived/dead state; wrapping it incorrectly with %v instead of %w, which silently disables the errors.Is check and causes normal retries instead.
Related errors
- revoke task
- task id conflicts with another task
- testutil: redis is down
- asynq: task lease expired
- handler not found for task
AI-assisted analysis of hibiken/asynq@d135f1439b (2026-09-07).
Data as JSON: /api/errors/ef7be40ed829723e.
Report an issue: GitHub.
Appendix: source
Thrown at processor.go:329
ctx, cancel := context.WithDeadline(context.Background(), l.Deadline())
defer cancel()
err := p.broker.Done(ctx, msg)
if err != nil {
errMsg := fmt.Sprintf("Could not remove task id=%s type=%q from %q err: %+v", msg.ID, msg.Type, base.ActiveKey(msg.Queue), err)
p.logger.Warnf("%s; Will retry syncing", errMsg)
p.syncRequestCh <- &syncRequest{
fn: func() error {
return p.broker.Done(ctx, msg)
},
errMsg: errMsg,
deadline: l.Deadline(),
}
}
}
// SkipRetry is used as a return value from Handler.ProcessTask to indicate that
// the task should not be retried and should be archived instead.
var SkipRetry = errors.New("skip retry for the task")
// RevokeTask is used as a return value from Handler.ProcessTask to indicate that
// the task should not be retried or archived.
var RevokeTask = errors.New("revoke task")
func (p *processor) handleFailedMessage(ctx context.Context, l *base.Lease, msg *base.TaskMessage, err error) {
if p.errHandler != nil {
p.errHandler.HandleError(ctx, NewTaskWithHeaders(msg.Type, msg.Payload, msg.Headers), err)
}
switch {
case errors.Is(err, RevokeTask):
p.logger.Warnf("revoke task id=%s", msg.ID)
p.markAsDone(l, msg)
case msg.Retried >= msg.Retry || errors.Is(err, SkipRetry):
p.logger.Warnf("Retry exhausted for task id=%s", msg.ID)
p.archive(l, msg, err)
default:
p.retry(l, msg, err, p.isFailureFunc(err))View on GitHub (pinned to d135f1439b)