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

  1. Truncate the producer's timestamp to nanosecond precision (9 digits) before sending.
  2. Use standard formatters (time.RFC3339Nano) which emit at most 9 fractional digits.
  3. Validate timestamps client-side before putting them in headers.
  4. 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

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


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/21772053f02b6650. Report an issue: GitHub.