grpc/grpc-go · error

malformed duration : too many decimals

Error message

malformed duration %q: too many decimals

What it means

After stripping the trailing 's' and an optional leading '-', UnmarshalJSON splits the body on '.' with a limit of 3 (duration.go:74-77). If there is more than one '.' (i.e. SplitN yields more than 2 parts), the duration is rejected as having too many decimal points. The protobuf-JSON duration allows at most one decimal separator between whole and fractional seconds.

Solutions

  1. Use a single decimal point: "1.25s", "0.500s".
  2. Avoid thousands separators in durations.
  3. Validate the string with regex '^-?\d+(\.\d{1,9})?s$' before submitting.

Example fix

// before:
//   { "retryPolicy": { "initialBackoff": "0.5.5s" } }
//   // error: malformed duration "0.5.5s": too many decimals

// after:
//   { "retryPolicy": { "initialBackoff": "0.500s" } }
Defensive patterns

Strategy: validation

Validate before calling

package main

import (
	"fmt"
	"regexp"
)

var durationRE319 = regexp.MustCompile(`^-?\d+(\.\d{1,9})?s$`)

func validateDuration(s string) error {
	if !durationRE319.MatchString(s) {
		return fmt.Errorf("malformed duration %q: use one decimal point, e.g. \"0.5s\"", s)
	}
	return nil
}

// func main() { _ = validateDuration("0.500s") }

Try / catch

// Unmarshal into serviceconfig.Duration surfaces this error.
//
//   var d serviceconfig.Duration
//   if err := json.Unmarshal([]byte(`"`+raw+`"`), &d); err != nil {
//       if strings.Contains(err.Error(), "too many decimals") {
//           // collapse to a single decimal point and retry.
//       }
//   }

Prevention

When it happens

Trigger: Triggered when a duration string contains two or more '.' characters, e.g. "1.2.3s", "0.5.6s", or "1..5s". The split at line 74 yields len(ss) > 2.

Common situations: Typo in hand-written service config, locale/serialization producing extra decimal points, or a value formatted with both a thousands separator and a decimal separator.

Understand the failure class

Related errors


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

Appendix: source

Thrown at internal/serviceconfig/duration.go:76

}

// UnmarshalJSON unmarshals b as a duration JSON string into d.
func (d *Duration) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	if !strings.HasSuffix(s, "s") {
		return fmt.Errorf("malformed duration %q: missing seconds unit", s)
	}
	neg := false
	if s[0] == '-' {
		neg = true
		s = s[1:]
	}
	ss := strings.SplitN(s[:len(s)-1], ".", 3)
	if len(ss) > 2 {
		return fmt.Errorf("malformed duration %q: too many decimals", s)
	}
	// 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 {

View on GitHub (pinned to 0c51461d27)