temporalio/temporal · warning
invalid subsecond value %v
Error message
invalid subsecond value %v
What it means
After validating whole seconds, unmarshalTimestamp also bounds the subsecond component: it locates the '.' and the timezone marker and rejects timestamps whose fractional-second field is longer than 9 digits ('.999999999'). Longer or malformed fractional parts cannot be represented in nanosecond precision and are rejected with this error.
Source
Thrown at common/nexus/nexusrpc/timestamp.go:42
// unmarshalTimestamp unmarshals a string into a Time instance. Uses RFC 3339, with some extra validation to ensure that
// seconds and subseconds are with an expected range.
// Copied from https://github.com/protocolbuffers/protobuf-go/blob/0b2c87d84c27802dae7248480444e22421ba577d/encoding/protojson/well_known_types.go#L749C1-L826C2
func unmarshalTimestamp(s string) (time.Time, error) {
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return t, err
}
// Validate seconds.
secs := t.Unix()
if secs < minTimestampSeconds || secs > maxTimestampSeconds {
return t, fmt.Errorf("second value out of range: %v", secs)
}
// Validate subseconds.
i := strings.LastIndexByte(s, '.') // start of subsecond field
j := strings.LastIndexAny(s, "Z-+") // start of timezone field
if i >= 0 && j >= i && j-i > len(".999999999") {
return t, fmt.Errorf("invalid subsecond value %v", s)
}
return t, nil
}
View on GitHub (pinned to bde624efd1)
Solutions
- Truncate the producer's timestamp to nanosecond precision (9 digits) before sending.
- Use standard formatters (time.RFC3339Nano) which emit at most 9 fractional digits.
- Validate timestamps client-side before putting them in headers.
- If the value is not a real timestamp, fix the sender — this often indicates string concatenation bugs.
Example fix
// before
ts := fmt.Sprintf("%s.%sZ", datePart, nanoPart) // nanoPart has 12 digits
// after
ts := t.UTC().Format(time.RFC3339Nano) // at most 9 fractional digits Defensive patterns
Strategy: validation
Validate before calling
if i := strings.IndexByte(s, '.'); i >= 0 {
j := strings.IndexAny(s[i:], "Z-+")
if j < 0 || (j-1) > 9 {
return errors.New("subsecond precision exceeds 9 digits")
}
} Type guard
func isRFC3339Nano(s string) bool {
_, err := time.Parse(time.RFC3339Nano, s)
return err == nil && !strings.Contains(s, ".") || len(strings.SplitN(strings.Split(s, "Z")[0], ".", 2)) < 2 || len(strings.SplitN(strings.Split(s, "Z")[0], ".", 2)[1]) <= 9
} Try / catch
t, err := time.Parse(time.RFC3339Nano, raw)
if err != nil {
return fmt.Errorf("rejecting timestamp header %q: %w", raw, err)
} Prevention
- Use time.RFC3339Nano formatting which caps fractional digits at 9
- Truncate higher-precision clocks (monotonic/picosecond sources) before formatting
- Validate header format client-side before sending
- Reject nonconforming requests with a 400 at your gateway
When it happens
Trigger: A timestamp header value like 2026-01-02T10:00:00.1234567890123Z (more than 9 fractional digits) or a malformed string where the timezone/index arithmetic finds an oversized subsecond span, from ServeHTTP.
Common situations: Non-Go clients emitting picosecond precision timestamps; hand-built headers in tests or scripts; custom formatting with more than nanosecond digits.
Related errors
- second value out of range: %v
- link type is empty
- link type contains invalid char (valid chars: alphanumeric,
- empty operation name
- empty operation token
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/21772053f02b6650.
Report an issue: GitHub.