apache/beam · error

can't apply processing delay of less than a millisecond. Got

Error message

can't apply processing delay of less than a millisecond. Got: %v

What it means

AfterProcessingTime triggers quantize delays to whole milliseconds, so PlusDelay rejects delays smaller than one millisecond by panicking. This enforces the documented minimum resolution of processing-time trigger transforms.

Source

Thrown at sdks/go/pkg/beam/core/graph/window/trigger/trigger.go:158

// Eg. A period of 20 with an offset of 45 would have alignments at 5,25,45,65 etc.
// Timestamps would be transformed as follows: 0 to 5 would be transformed to 5,
// 6 to 25 would be transformed to 25, 26 to 45 would be transformed to 45, and so on.
type AlignToTransform struct {
	Period, Offset int64 // in milliseconds
}

// String implements the Stringer interface and returns trigger details as a string.
func (t *AlignToTransform) String() string {
	return fmt.Sprintf("%#v", t)
}

func (AlignToTransform) timestampTransform() {}

// PlusDelay configures an AfterProcessingTime trigger to fire after a specified delay,
// no smaller than a millisecond.
func (t *AfterProcessingTimeTrigger) PlusDelay(delay time.Duration) *AfterProcessingTimeTrigger {
	if delay < time.Millisecond {
		panic(fmt.Errorf("can't apply processing delay of less than a millisecond. Got: %v", delay))
	}
	t.timestampTransforms = append(t.timestampTransforms, DelayTransform{Delay: int64(delay / time.Millisecond)})
	return t
}

// AlignedTo configures an AfterProcessingTime trigger to fire
// at the smallest multiple of period since the offset greater than the first element timestamp.
//
// * Period may not be smaller than a millisecond.
// * Offset may be a zero time (time.Time{}).
func (t *AfterProcessingTimeTrigger) AlignedTo(period time.Duration, offset time.Time) *AfterProcessingTimeTrigger {
	if period < time.Millisecond {
		panic(fmt.Errorf("can't apply an alignment period of less than a millisecond. Got: %v", period))
	}
	offsetMillis := int64(0)
	if !offset.IsZero() {
		// TODO: Change to call UnixMilli() once we move to only supporting a go version > 1.17.
		offsetMillis = offset.Unix()*1e3 + int64(offset.Nanosecond())/1e6

View on GitHub (pinned to 12126d8942)

Solutions

  1. Use a delay of at least time.Millisecond.
  2. Clamp the computed delay to time.Millisecond as a floor before calling PlusDelay.
  3. If sub-millisecond timing is truly required, this trigger API cannot support it; redesign around a coarser granularity.
  4. Verify duration arithmetic units (ns-based time.Duration values are easily mis-scaled).

Example fix

// before
t := trigger.AfterProcessingTime().PlusDelay(500 * time.Microsecond)
// after
d := 500 * time.Microsecond
if d < time.Millisecond { d = time.Millisecond }
t := trigger.AfterProcessingTime().PlusDelay(d)
Defensive patterns

Strategy: validation

Validate before calling

if delay < time.Millisecond { return fmt.Errorf("delay must be >= 1ms, got %v", delay) }

Try / catch

defer func() {
    if r := recover(); r != nil {
        if strings.Contains(fmt.Sprint(r), "processing delay") { return ErrDelayTooSmall }
        panic(r)
    }
}()

Prevention

When it happens

Trigger: Calling PlusDelay with a duration below time.Millisecond, e.g. PlusDelay(500 * time.Microsecond) or a sub-millisecond value computed at runtime.

Common situations: Using nanosecond/microsecond constants intended for other timing APIs; arithmetic on durations (like dividing a second by 10000) producing sub-ms values; porting code from systems with finer trigger resolution.

Understand the failure class

Background: "invalid duration" / "failed to parse duration": why your timeout, interval, or TTL string is rejected and which formats each library accepts — this error's family across 32 libraries.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/3b7640653a0e2308. Report an issue: GitHub.