grpc/grpc-go · error

malformed duration %q: too many digits after decimal

Error message

malformed duration %q: too many digits after decimal

What it means

Returned by Duration.UnmarshalJSON when the fractional (post-decimal) portion of the duration string has more than 9 digits. Nanosecond precision is the maximum the protobuf Duration spec and Go's time.Duration support, so additional digits are rejected rather than silently truncated.

Source

Thrown at internal/serviceconfig/duration.go:96

	// hasDigits is set if either the whole or fractional part of the number is
	// present, since both are optional but one is required.
	hasDigits := false
	var sec, ns int64
	if len(ss[0]) > 0 {
		var err error
		if sec, err = strconv.ParseInt(ss[0], 10, 64); err != nil {
			return fmt.Errorf("malformed duration %q: %v", s, err)
		}
		// Maximum seconds value per the durationpb spec.
		const maxProtoSeconds = 315_576_000_000
		if sec > maxProtoSeconds {
			return fmt.Errorf("out of range: %q", s)
		}
		hasDigits = true
	}
	if len(ss) == 2 && len(ss[1]) > 0 {
		if len(ss[1]) > 9 {
			return fmt.Errorf("malformed duration %q: too many digits after decimal", s)
		}
		var err error
		if ns, err = strconv.ParseInt(ss[1], 10, 64); err != nil {
			return fmt.Errorf("malformed duration %q: %v", s, err)
		}
		for i := 9; i > len(ss[1]); i-- {
			ns *= 10
		}
		hasDigits = true
	}
	if !hasDigits {
		return fmt.Errorf("malformed duration %q: contains no numbers", s)
	}

	if neg {
		sec *= -1
		ns *= -1
	}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Trim the fractional part to at most 9 digits, e.g. "1.123456789s".
  2. Round the source value to nanosecond precision before serializing.
  3. Generate durations via serviceconfig.Duration.MarshalJSON which always emits 3/6/9 digits.

Example fix

// before
{"timeout": "0.1234567890s"}

// after
{"timeout": "0.123456789s"}
Defensive patterns

Strategy: validation

Validate before calling

func validateProtoDuration(s string) error {
    body := strings.TrimSuffix(s, "s")
    parts := strings.SplitN(body, ".", 2)
    if len(parts) == 2 && len(parts[1]) > 9 {
        return fmt.Errorf("too many fractional digits in %q", s)
    }
    var d serviceconfig.Duration
    return d.UnmarshalJSON([]byte(`"` + s + `"`))
}

Type guard

func fractionalDigitsAtMost9(s string) bool {
    body := strings.TrimSuffix(s, "s")
    parts := strings.SplitN(body, ".", 2)
    return len(parts) != 2 || len(parts[1]) <= 9
}

Prevention

When it happens

Trigger: A value like "1.1234567890s" (10 fractional digits) triggers `if len(ss[1]) > 9` at duration.go:95. Typical when a high-precision timestamp or arbitrary float is written as a duration.

Common situations: Tooling that emits full float64 precision; copy/paste of a nanosecond timestamp with sub-ns digits; localized formatting that pads decimals.

Understand the failure class

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/e2a33d383c3cd8de. Report an issue: GitHub.