hibiken/asynq · error

task ID conflicts with another task

Error message

task ID conflicts with another task

What it means

ErrTaskIDConflict indicates the task could not be enqueued because another task with the same TaskID already exists (in active/pending/scheduled state). It only applies to tasks enqueued with a TaskID option. The client wraps the internal errors.ErrTaskIdConflict into this public sentinel.

Solutions

  1. Check errors.Is(err, asynq.ErrTaskIDConflict) and skip/requeue later — the task already exists.
  2. Use a unique ID (UUID) per task instead of reusing stable business IDs.
  3. Wait for the existing task to finish and its retention period to expire before reusing the ID.
  4. Shorten the Retention duration on the original task so the ID frees up sooner.

Example fix

// before
_, err := client.Enqueue(task, asynq.TaskID(userID))
// after
_, err := client.Enqueue(task, asynq.TaskID(fmt.Sprintf("sync:%s:%d", userID, time.Now().Unix())))
Defensive patterns

Strategy: try-catch

Validate before calling

// Check whether an ID-derived task may still exist before enqueue:
if activeTaskIDs[taskID] { return ErrTaskInProgress }

Type guard

func isTaskIDConflict(err error) bool { return errors.Is(err, asynq.ErrTaskIDConflict) }

Try / catch

if _, err := client.Enqueue(task, asynq.TaskID(id)); err != nil {
    if isTaskIDConflict(err) { return ErrAlreadySubmitted }
    return err
}

Prevention

When it happens

Trigger: Calling client.Enqueue with asynq.TaskID(id) where a task with that exact ID still exists in any queue state (pending, active, scheduled, retry, archived, or within retention).

Common situations: Using user/entity IDs as TaskID without allowing for task completion time; re-enqueuing after retention-expiry assumptions are wrong; two producers picking the same deterministic TaskID.

Related errors


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

Appendix: source

Thrown at client.go:253

}

func (h headerOption) String() string {
	var bytes []byte
	bytes, _ = json.Marshal(h)
	return fmt.Sprintf("Header(%s)", bytes)
}
func (h headerOption) Type() OptionType   { return HeaderOpt }
func (h headerOption) Value() interface{} { return [2]string{h[0], h[1]} }

// ErrDuplicateTask indicates that the given task could not be enqueued since it's a duplicate of another task.
//
// ErrDuplicateTask error only applies to tasks enqueued with a Unique option.
var ErrDuplicateTask = errors.New("task already exists")

// ErrTaskIDConflict indicates that the given task could not be enqueued since its task ID already exists.
//
// ErrTaskIDConflict error only applies to tasks enqueued with a TaskID option.
var ErrTaskIDConflict = errors.New("task ID conflicts with another task")

type option struct {
	retry     int
	queue     string
	taskID    string
	timeout   time.Duration
	deadline  time.Time
	uniqueTTL time.Duration
	processAt time.Time
	retention time.Duration
	group     string
	headers   map[string]string
}

// composeOptions merges user provided options into the default options
// and returns the composed option.
// It also validates the user provided options and returns an error if any of
// the user provided options fail the validations.

View on GitHub (pinned to d135f1439b)