apache/beam · error · IllegalStateException

Outbound timers endpoint already registered for ${timerKey}

Error message

Outbound timers endpoint already registered for ${timerKey}

What it means

Analogous to the data endpoint check: registerOutputTimersLocation registers one outbound timers receiver per (pTransformId, timerFamilyId) pair. This IllegalStateException is thrown when the same TimerEndpoint is registered twice, which would mean two receivers competing for the same timers channel.

Source

Thrown at sdks/java/core/src/main/java/org/apache/beam/sdk/fn/data/BeamFnDataOutboundAggregator.java:168

        checkFlushThreadException();
        synchronized (flushLock) {
          receiver.accept(data);
        }
      };
    }
    outputDataReceivers.put(pTransformId, receiver);
    return receiver;
  }

  /**
   * Register the outbound timers logical endpoint, returns the FnDataReceiver for processing the
   * endpoint's outbound timers data.
   */
  public <T> FnDataReceiver<T> registerOutputTimersLocation(
      String pTransformId, String timerFamilyId, Coder<T> coder) {
    TimerEndpoint timerKey = new TimerEndpoint(pTransformId, timerFamilyId);
    if (outputTimersReceivers.containsKey(timerKey)) {
      throw new IllegalStateException(
          "Outbound timers endpoint already registered for " + timerKey);
    }
    Receiver<T> receiver = new Receiver<>(coder);
    if (timeLimit > 0) {
      outputTimersReceivers.put(timerKey, receiver);
      return timers -> {
        checkFlushThreadException();
        synchronized (flushLock) {
          receiver.accept(timers);
        }
      };
    }
    outputTimersReceivers.put(timerKey, receiver);
    return receiver;
  }

  private void flushInternal() {
    if (bytesWrittenSinceFlush == 0) {

View on GitHub (pinned to 12126d8942)

Solutions

  1. Ensure each (pTransformId, timerFamilyId) pair is registered only once per bundle
  2. Verify the timer family id is distinct where multiple timers per transform are intended
  3. Check bundle setup code for double invocation of addOutgoingTimersEndpoint
  4. Recreate the aggregator between bundles so stale registrations are cleared

Example fix

// before
aggregator.registerOutputTimersLocation(pTransformId, timerFamilyId, coder);
aggregator.registerOutputTimersLocation(pTransformId, timerFamilyId, coder); // throws
// after
aggregator.registerOutputTimersLocation(pTransformId, timerFamilyId + "-v2", coder); // distinct family id
Defensive patterns

Strategy: validation

Validate before calling

if (registeredTimerKeys.contains(pTransformId + "/" + timerFamilyId)) {
  return; // skip duplicate registration
}
registeredTimerKeys.add(pTransformId + "/" + timerFamilyId);

Try / catch

try {
  aggregator.registerOutputTimersLocation(pTransformId, timerFamilyId, coder);
} catch (IllegalStateException e) {
  LOG.warn("timer endpoint already registered: " + timerFamilyId, e);
}

Prevention

When it happens

Trigger: Calling registerOutputTimersLocation twice with identical pTransformId and timerFamilyId; duplicate wiring through addOutgoingTimersEndpoint or registerOutputLocation for the same timer family.

Common situations: Stateful DoFns with multiple timer families whose registration code runs twice per bundle; runner harness re-initialization without resetting outputTimersReceivers between bundles.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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