grpc/grpc-go · error

malformed duration : too many digits after decimal

Error message

malformed duration %q: too many digits after decimal

What it means

Fires at duration.go:96 when the fractional (sub-second) part of a protobuf Duration JSON string has more than 9 digits. The protobuf Duration type only carries nanosecond precision, so more than 9 fractional digits cannot be represented and is rejected rather than silently truncated.

Solutions

  1. Trim the fractional part of the offending value to at most 9 digits.
  2. If you control the producer, format with %09d on the nanosecond component (the library's own MarshalJSON trims trailing zeros to 0/3/6/9 digits).
  3. Round the source time.Duration to nanosecond precision before serializing.

Example fix

// before
// "timeout": "1.1234567890s"   // 10 fractional digits

// after
// "timeout": "1.123456789s"      // 9 digits max (nanosecond precision)
Defensive patterns

Strategy: validation

Validate before calling

func formatDuration(d time.Duration) string {
    // Trim to <= 9 fractional digits; library MarshalJSON already does this.
    b, _ := json.Marshal(time.Duration(d)) // use canonical encoding
    return string(b)
}

Try / catch

if err := json.Unmarshal(rawConfig, &cfg); err != nil {
    if strings.Contains(err.Error(), "too many digits after decimal") {
        // trim the fractional part to 9 digits and retry
    }
}

Prevention

When it happens

Trigger: A Duration string whose decimal portion (ss[1] after splitting on '.') has length > 9. Examples: "1.1234567890s" (10 digits), "0.0000000001s", or a value formatted by code that pads fractions to fixed widths greater than 9.

Common situations: A serializer that emits microsecond timestamps as Duration strings with 12-15 fractional digits; config generated from time.Duration.Nanoseconds() formatting with too many digits; values copied from a system that uses picosecond/femtosecond precision.

Understand the failure class

Related errors


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

Appendix: 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 0c51461d27)