hashicorp/nomad · warning

Update max parallel count is greater than task group count (

Error message

Update max parallel count is greater than task group count (%d > %d). A destructive change would result in the simultaneous replacement of all allocations.

What it means

The update strategy's max_parallel exceeds the task group's count (when count > 1), meaning a deployment could replace every allocation at once. Nomad flags this as a warning-class validation error because it defeats rolling-update safety.

Source

Thrown at nomad/structs/structs.go:7647

	if int64(tg.Count) < tg.Scaling.Min && !(j.IsMultiregion() && tg.Count == 0 && j.Region == "global") {
		mErr.Errors = append(mErr.Errors,
			fmt.Errorf("Scaling policy invalid: task group count must not be less than minimum count in scaling policy"))
	}

	return mErr.ErrorOrNil()
}

// Warnings returns a list of warnings that may be from dubious settings or
// deprecation warnings.
func (tg *TaskGroup) Warnings(j *Job) error {
	var mErr multierror.Error

	// Validate the update strategy
	if u := tg.Update; u != nil {
		// Check the counts are appropriate
		if tg.Count > 1 && u.MaxParallel > tg.Count && !(j.IsMultiregion() && tg.Count == 0) {
			mErr.Errors = append(mErr.Errors,
				fmt.Errorf("Update max parallel count is greater than task group count (%d > %d). "+
					"A destructive change would result in the simultaneous replacement of all allocations.", u.MaxParallel, tg.Count))
		}
	}

	if tg.MaxClientDisconnect != nil {
		mErr.Errors = append(mErr.Errors, errors.New("MaxClientDisconnect is deprecated and ignored in favor of Disconnect.LostAfter"))
	}

	if tg.StopAfterClientDisconnect != nil {
		mErr.Errors = append(mErr.Errors, errors.New("StopAfterClientDisconnect is deprecated and ignored favor of Disconnect.StopOnClientAfter"))
	}

	if tg.PreventRescheduleOnLost {
		mErr.Errors = append(mErr.Errors, errors.New("PreventRescheduleOnLost is deprecated and ignored in favor of Disconnect.Replace"))
	}

	// Warn about unbounded rescheduling which may cause thrashing if tasks have
	// unlimited attempts and a low delay.

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Reduce `max_parallel` to be <= the group count (typically 1-3)
  2. Increase the group `count` if a high parallelism rollout is intended
  3. Remove the update block for single-instance/dev groups

Example fix

// before
count = 2
update { max_parallel = 5 min_healthy_time = "10s" }
// after
count = 2
update { max_parallel = 1 min_healthy_time = "10s" }
Defensive patterns

Strategy: validation

Validate before calling

if u := g.Update; u != nil && g.Count > 1 && u.MaxParallel > g.Count {
  return fmt.Errorf("max_parallel %d > count %d", u.MaxParallel, g.Count)
}

Type guard

func safeUpdate(g *api.TaskGroup) bool {
  return g.Update == nil || g.Count <= 1 || g.Update.MaxParallel <= g.Count
}

Prevention

When it happens

Trigger: Setting `update { max_parallel = N }` where N > tg.Count and the group is not a multiregion group with count 0, e.g. count=2 with max_parallel=5.

Common situations: Copy-pasting update blocks with max_parallel=10 into small groups; shrinking a group's count after autoscaling reduced instances while keeping a large max_parallel; canary/stagger values tuned for big fleets applied to dev jobs.

Related errors


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