apache/beam · error

batch.groupIntoBatchesFn: unexpected timer family %q

Error message

batch.groupIntoBatchesFn: unexpected timer family %q

What it means

This panic fires in groupIntoBatchesFn.OnTimer when a timer callback arrives whose family string does not match the WindowEnd timer spec declared by the DoFn. The transform only schedules one timer family (fn.WindowEnd.Family) for lateness/flush handling, so any other family indicates misconfigured or foreign timers on the same key — an internal consistency check that panics loudly. Typical root causes are leftover timers from a previous pipeline version or a runner delivering timers from an unrelated spec.

Source

Thrown at sdks/go/pkg/beam/transforms/batch/batch.go:305

		newBytes = cur
	}

	if fn.BatchSize > 0 && count >= fn.BatchSize {
		fn.flush(sp, key, emit)
		return
	}
	if fn.BatchSizeBytes > 0 && newBytes >= fn.BatchSizeBytes {
		fn.flush(sp, key, emit)
		return
	}
}

func (fn *groupIntoBatchesFn) OnTimer(
	ctx context.Context, ts beam.EventTime, sp state.Provider, tp timers.Provider,
	key typex.T, timer timers.Context, emit func(typex.T, []typex.V),
) {
	if timer.Family != fn.WindowEnd.Family {
		panic(fmt.Sprintf("batch.groupIntoBatchesFn: unexpected timer family %q", timer.Family))
	}
	fn.codec.init(fn.ValueType.T)
	fn.flush(sp, key, emit)
}

func (fn *groupIntoBatchesFn) flush(
	sp state.Provider, key typex.T, emit func(typex.T, []typex.V),
) {
	buf, ok, err := fn.Buffer.Read(sp)
	if err != nil {
		panic(err)
	}
	if !ok || len(buf) == 0 {
		return
	}

	out := make([]typex.V, len(buf))
	for i, b := range buf {

View on GitHub (pinned to 12126d8942)

Solutions

  1. Check timer.Family in logs against fn.WindowEnd.Family; identify which spec scheduled the unexpected timer.
  2. After renaming the timer spec, drain or clear old timers (or use a fresh state/timer namespace) before redeploying.
  3. Make sure only groupIntoBatchesFn schedules timers on the same key/state namespace.
  4. If intentional multi-family timers are needed, extend OnTimer to switch on the family instead of panicking.

Example fix

// before
if timer.Family != fn.WindowEnd.Family {
	panic(fmt.Sprintf("batch.groupIntoBatchesFn: unexpected timer family %q", timer.Family))
}
// after: ignore foreign timers instead of aborting the bundle
if timer.Family != fn.WindowEnd.Family {
	return // not our timer; ignore
}
Defensive patterns

Strategy: validation

Validate before calling

// Before deploying a renamed timer spec, assert no old-family timers remain:
if timer.Family != expectedFamily {
	log.Printf("ignoring foreign timer family %q (expected %q)", timer.Family, expectedFamily)
	return
}

Type guard

func isWindowEndTimer(fn *groupIntoBatchesFn, t timers.Context) bool {
	return t.Family == fn.WindowEnd.Family
}

Try / catch

func safeOnTimer(fn *groupIntoBatchesFn, timer timers.Context) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("on-timer dispatch failed: %v", r)
		}
	}()
	// call OnTimer
	return nil
}

Prevention

When it happens

Trigger: OnTimer is invoked with timers.Context whose timer.Family differs from fn.WindowEnd.Family — e.g. after renaming the timer spec while old timers from a previous job version are still queued, or a misconfigured pipeline feeding this DoFn timers it never set.

Common situations: Developers hit this after modifying the timer family/identifier in an upgraded pipeline while reusing state (old timers still pending), or when a custom runner/test harness dispatches timers with a default family name.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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