t8y2/dbx · error
ttl must be a positive integer
Error message
ttl must be a positive integer
What it means
put validates that a supplied TTL is a positive integer. Since etcd v2 TTLs are seconds sent in the form payload, zero or negative values are meaningless and rejected before the request is built. This check runs only when ttl was actually provided.
Source
Thrown at agents/drivers/etcd2-go/kv.go:212
preserveLease := boolOrDefault(params, "preserveLease", false)
leaseValue := longOrNull(params, "lease")
hasLease := leaseValue != nil
hasTtl := ttlValue != nil
if (hasLease && hasTtl) || (preserveLease && (hasLease || hasTtl)) {
return nil, errors.New("lease, ttl, and preserveLease cannot be specified together")
}
if hasLease {
return nil, errors.New("ETCD_V2_LEASE_UNSUPPORTED: the etcd v2 API has no lease objects; use ttl instead")
}
if preserveLease {
return nil, errors.New("ETCD_V2_LEASE_UNSUPPORTED: the etcd v2 API has no lease objects; use ttl instead")
}
if expectedCreateRevision != nil && *expectedCreateRevision != 0 {
return nil, errors.New("ETCD_V2_CAS_UNSUPPORTED: the etcd v2 API cannot compare createdIndex; use expectedModRevision")
}
if hasTtl && *ttlValue <= 0 {
return nil, errors.New("ttl must be a positive integer")
}
form := url.Values{}
form.Set("value", value)
if hasTtl {
form.Set("ttl", strconv.FormatInt(*ttlValue, 10))
}
if expectedModRevision != nil {
form.Set("prevIndex", strconv.FormatInt(*expectedModRevision, 10))
}
if expectedCreateRevision != nil && *expectedCreateRevision == 0 {
form.Set("prevExist", "false")
}
ctx, cancel := s.beginOperation()
defer s.endOperation(cancel)
body, _, err := client.do(ctx, http.MethodPut, v2KeyPath(key), form.Encode(), nil)
if err != nil {View on GitHub (pinned to c0390bff16)
Solutions
- Omit the ttl parameter entirely for keys that should not expire
- Send ttl >= 1 (seconds)
- Fix defaults so 'unset' is represented by a nil/absent ttl, not 0 or -1
Example fix
// before
{ "key":"k", "value":"v", "ttl": 0 }
// after
{ "key":"k", "value":"v" } // omit ttl for no expiry Defensive patterns
Strategy: validation
Validate before calling
if ttl != nil && *ttl <= 0 {
return errors.New("ttl must be >= 1; omit it for no expiry")
} Try / catch
_, err := session.handle(ctx, "put", params)
if err != nil && strings.Contains(err.Error(), "ttl must be a positive integer") {
return fmt.Errorf("fix ttl (got %v): use >=1 or omit the field", params["ttl"])
} Prevention
- Use typed ttl params so 0/negatives are caught at compile time where possible
- Represent 'no expiry' by omitting ttl, never by ttl:0
- Clamp or reject non-positive ttl in your request builder
- Add table tests for ttl edge values 0, -1, 1
When it happens
Trigger: Calling put with ttl = 0 or ttl < 0 (ttl absent skips the check entirely).
Common situations: Using ttl:0 to mean 'no expiry' (the correct way is to omit the field); integer division or defaulting logic producing 0; config defaults of -1 intended as 'unset'.
Understand the failure class
Background: "must be positive", "Invalid value": how libraries reject invalid parameter values (ValueError, ArgumentError, INVALID_PARAMETER_VALUE) — this error's family across 28 libraries.
Related errors
- ttl must be a positive integer
- lease, ttl, and preserveLease cannot be specified together
- ETCD_WATCH_SCOPE_INVALID
- ETCD_INVALID_%s
- ETCD_%s_REQUIRED
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/185e8e01abee725e.
Report an issue: GitHub.