hibiken/asynq · error

Unique TTL cannot be less than 1s

Error message

Unique TTL cannot be less than 1s

What it means

Raised in composeOptions when a uniqueOption TTL is less than 1 second. Uniqueness locks are checked with second-level granularity, so a sub-second TTL would be meaningless; the client rejects it upfront rather than enqueueing with a broken uniqueness guarantee.

Solutions

  1. Use a TTL of at least 1*time.Second.
  2. If you need millisecond-scale dedup, implement your own lock (e.g. Redis SETNX) instead of Unique.
  3. Fix config unit conversion so the value is seconds-based.

Example fix

// before
asynq.Unique(500 * time.Millisecond)
// after
asynq.Unique(1 * time.Second)
Defensive patterns

Strategy: validation

Validate before calling

const minUniqueTTL = time.Second
if ttl < minUniqueTTL { return fmt.Errorf("unique TTL %v below minimum %v", ttl, minUniqueTTL) }

Type guard

func validUniqueTTL(d time.Duration) bool { return d >= time.Second }

Try / catch

// Prefer validation; otherwise treat as programmer error:
if strings.Contains(err.Error(), "Unique TTL cannot be less than 1s") { log.Fatal("fix unique TTL config") }

Prevention

When it happens

Trigger: Calling client.Enqueue(task, asynq.Unique(500*time.Millisecond)) or Unique(0), or passing a duration computed from config that resolves below 1s (e.g. Unique(ttl) with ttl in ms units).

Common situations: Confusing milliseconds with seconds when loading TTL from config; passing time.Duration(intValue) where intValue was meant as milliseconds; wanting very short dedup windows.

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/e5fa5de4e6d583e8. Report an issue: GitHub.

Appendix: source

Thrown at client.go:305

			qname := string(opt)
			if err := base.ValidateQueueName(qname); err != nil {
				return option{}, err
			}
			res.queue = qname
		case taskIDOption:
			id := string(opt)
			if isBlank(id) {
				return option{}, errors.New("task ID cannot be empty")
			}
			res.taskID = id
		case timeoutOption:
			res.timeout = time.Duration(opt)
		case deadlineOption:
			res.deadline = time.Time(opt)
		case uniqueOption:
			ttl := time.Duration(opt)
			if ttl < 1*time.Second {
				return option{}, errors.New("Unique TTL cannot be less than 1s")
			}
			res.uniqueTTL = ttl
		case processAtOption:
			res.processAt = time.Time(opt)
		case processInOption:
			res.processAt = time.Now().Add(time.Duration(opt))
		case retentionOption:
			res.retention = time.Duration(opt)
		case groupOption:
			key := string(opt)
			if isBlank(key) {
				return option{}, errors.New("group key cannot be empty")
			}
			res.group = key
		case headerOption:
			key, value := opt[0], opt[1]
			res.headers[key] = value
		default:

View on GitHub (pinned to d135f1439b)