hashicorp/nomad · error

retry config backoff %d is greater than max_backoff %d

Error message

retry config backoff %d is greater than max_backoff %d

What it means

RetryConfig.Validate rejects a RetryConfig whose Backoff exceeds its explicit MaxBackoff; setting max_backoff=0 disables the check because 0 means unbounded. The error embeds both raw nanosecond values so the offending pair is visible.

Source

Thrown at client/config/config.go:857

	// If Backoff not set, no need to validate
	if rc.Backoff == nil {
		return nil
	}

	// MaxBackoff nil will end up defaulted to 1 minutes. We should validate that
	// the user supplied backoff does not exceed that.
	if rc.MaxBackoff == nil && *rc.Backoff > config.DefaultRetryMaxBackoff {
		return fmt.Errorf("retry config backoff %d is greater than default max_backoff %d", *rc.Backoff, config.DefaultRetryMaxBackoff)
	}

	// MaxBackoff == 0 means backoff is unbounded. No need to validate.
	if rc.MaxBackoff != nil && *rc.MaxBackoff == 0 {
		return nil
	}

	if rc.MaxBackoff != nil && *rc.Backoff > *rc.MaxBackoff {
		return fmt.Errorf("retry config backoff %d is greater than max_backoff %d", *rc.Backoff, *rc.MaxBackoff)
	}

	return nil
}

// Merge merges two RetryConfigs. The passed instance always takes precedence.
func (rc *RetryConfig) Merge(b *RetryConfig) *RetryConfig {
	if rc == nil {
		return b
	}

	result := *rc
	if b == nil {
		return &result
	}

	if b.Attempts != nil {
		result.Attempts = &*b.Attempts

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Raise max_backoff to be >= backoff, e.g. backoff="30s" max_backoff="5m"
  2. Lower backoff below the configured max_backoff
  3. Set max_backoff = 0 explicitly if unbounded backoff is intended and the backoff value is deliberate
  4. Validate the merged RetryConfig after Merge to catch inversions introduced by overrides

Example fix

// before
retry {
  backoff = "5m"
  max_backoff = "1m"
}
// after
retry {
  backoff = "30s"
  max_backoff = "5m"
}
Defensive patterns

Strategy: validation

Validate before calling

if rc.Backoff != nil && rc.MaxBackoff != nil && *rc.MaxBackoff != 0 && *rc.Backoff > *rc.MaxBackoff {
    return fmt.Errorf("backoff %s > max_backoff %s", *rc.Backoff, *rc.MaxBackoff)
}

Type guard

func retryBoundsOK(rc *config.RetryConfig) bool {
    return rc == nil || rc.Backoff == nil || rc.MaxBackoff == nil || *rc.MaxBackoff == 0 || *rc.Backoff <= *rc.MaxBackoff
}

Try / catch

if err := rc.Validate(); err != nil {
    if strings.Contains(err.Error(), "greater than max_backoff") {
        return fmt.Errorf("fix retry block: backoff must be <= max_backoff (or set max_backoff=0 for unbounded): %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling RetryConfig.Validate when both Backoff and MaxBackoff are non-nil, MaxBackoff != 0, and *Backoff > *MaxBackoff, e.g. backoff="5m" max_backoff="1m"; also after Merge combines configs producing an inverted pair.

Common situations: retry block in agent config with max_backoff smaller than backoff; layered config merge where an override sets a lower max_backoff; programmatic construction setting fields independently without ordering checks.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/144eb0ab6705ca00. Report an issue: GitHub.