hashicorp/nomad · error

cannot update expiration TTL

Error message

cannot update expiration TTL

What it means

A validation error from ACLToken.Validate on update: a token's ExpirationTTL is immutable after creation. Nomad rejects updates where the submitted TTL differs from the existing token's ExpirationTTL.

Solutions

  1. Leave ExpirationTTL unchanged when updating the token
  2. To change lifetime, create a new token with the desired TTL and migrate workloads
  3. Copy ExpirationTTL verbatim from the existing token into the update request

Example fix

// before
update := existing
update.ExpirationTTL = 72 * time.Hour // immutable
// after
update := existing // ExpirationTTL unchanged
newToken := &api.ACLToken{Name: existing.Name, Type: existing.Type, Policies: existing.Policies, ExpirationTTL: 72 * time.Hour}
apiClient.ACL().Create(newToken, nil)
Defensive patterns

Strategy: validation

Validate before calling

if existing != nil && existing.ExpirationTTL != update.ExpirationTTL {
    return errors.New("ExpirationTTL is immutable; create a new token to change lifetime")
}

Type guard

func expirationTTLStable(existing, update *structs.ACLToken) bool {
    return existing == nil || existing.ExpirationTTL == update.ExpirationTTL
}

Try / catch

if err := update.Validate(minTTL, maxTTL, existing); err != nil {
    if strings.Contains(err.Error(), "cannot update expiration TTL") {
        update.ExpirationTTL = existing.ExpirationTTL
        err = update.Validate(minTTL, maxTTL, existing)
    }
}

Prevention

When it happens

Trigger: Calling ACL Upsert (update) on an existing token with a modified ExpirationTTL value (including zeroing it or setting it on a token created without one).

Common situations: Automation extending/renewing token lifetime by editing the TTL; token renewal scripts that mistakenly update instead of recreating; config drift tools rewriting all fields to defaults.

Related errors


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

Appendix: source

Thrown at nomad/structs/acl.go:815

			// check this against the regions max and min values.
			expiresIn := a.ExpirationTime.Sub(a.CreateTime)
			if expiresIn > maxTTL {
				mErr.Errors = append(mErr.Errors,
					fmt.Errorf("expiration time cannot be more than %s in the future (was %s)",
						maxTTL, expiresIn))

			} else if expiresIn < minTTL {
				mErr.Errors = append(mErr.Errors,
					fmt.Errorf("expiration time cannot be less than %s in the future (was %s)",
						minTTL, expiresIn))
			}
		}
	default:
		if existing.Global != a.Global {
			mErr.Errors = append(mErr.Errors, errors.New("cannot toggle global mode"))
		}
		if existing.ExpirationTTL != a.ExpirationTTL {
			mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
		}
		if a.ExpirationTime != nil {
			if !existing.ExpirationTime.Equal(*a.ExpirationTime) {
				mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
			}
		}

	}

	return mErr.ErrorOrNil()
}

// HasExpirationTime checks whether the ACL token has an expiration time value
// set.
func (a *ACLToken) HasExpirationTime() bool {
	if a == nil || a.ExpirationTime == nil {
		return false
	}

View on GitHub (pinned to 482b49bf1a)