apache/beam · error · IllegalArgumentException

Start time cannot be after end time.

Error message

Start time cannot be after end time.

What it means

SpannerIO.readChangeStream() validates that the inclusive start time is not after the inclusive end time, comparing their java.sql.Timestamp values. If inclusiveEndAt is set and start is later than end, it throws IllegalArgumentException because Spanner change streams require a monotonically ordered query window.

Source

Thrown at sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java:2258

          getChangeStreamName() != null,
          "SpannerIO.readChangeStream() requires the name of the change stream to be set.");
      checkArgument(
          getInclusiveStartAt() != null,
          "SpannerIO.readChangeStream() requires the start time to be set.");
      // Inclusive end at is defaulted to ChangeStreamsContants.MAX_INCLUSIVE_END_AT
      checkArgument(
          getInclusiveEndAt() != null,
          "SpannerIO.readChangeStream() requires the end time to be set. If you'd like to process the stream without an end time, you can omit this parameter.");
      if (getMetadataInstance() != null) {
        checkArgument(
            getMetadataDatabase() != null,
            "SpannerIO.readChangeStream() requires the metadata database to be set if metadata instance is set.");
      }

      // Start time must be before end time
      if (getInclusiveEndAt() != null
          && getInclusiveStartAt().toSqlTimestamp().after(getInclusiveEndAt().toSqlTimestamp())) {
        throw new IllegalArgumentException("Start time cannot be after end time.");
      }

      final DatabaseId changeStreamDatabaseId =
          DatabaseId.of(
              getSpannerConfig().getProjectId().get(),
              getSpannerConfig().getInstanceId().get(),
              getSpannerConfig().getDatabaseId().get());
      final String partitionMetadataInstanceId =
          MoreObjects.firstNonNull(
              getMetadataInstance(), changeStreamDatabaseId.getInstanceId().getInstance());
      final String partitionMetadataDatabaseId =
          MoreObjects.firstNonNull(getMetadataDatabase(), changeStreamDatabaseId.getDatabase());

      final SpannerConfig changeStreamSpannerConfig = buildChangeStreamSpannerConfig();
      final SpannerConfig partitionMetadataSpannerConfig =
          MetadataSpannerConfigFactory.create(
              changeStreamSpannerConfig, partitionMetadataInstanceId, partitionMetadataDatabaseId);
      final Dialect changeStreamDatabaseDialect =

View on GitHub (pinned to 12126d8942)

Solutions

  1. Swap or correct start/end so start <= end before building the transform
  2. Validate ordering in your code before calling changeStreamStartAt/changeStreamEndAt
  3. Normalize timezones/units so both values are converted via the same toSqlTimestamp path

Example fix

// before
.changeStreamStartAt(laterTime).changeStreamEndAt(earlierTime)
// after
if (start.after(end)) { /* fix or swap */ }
.changeStreamStartAt(earlierTime).changeStreamEndAt(laterTime)
Defensive patterns

Strategy: validation

Validate before calling

if (end != null && start.toSqlTimestamp().after(end.toSqlTimestamp())) {
  throw new IllegalArgumentException("Start time cannot be after end time.");
}

Try / catch

try { pipeline.apply(readChangeStream); } catch (IllegalArgumentException e) { LOG.error("Invalid change stream window: {}", e.getMessage()); }

Prevention

When it happens

Trigger: Building ReadChangeStream with .changeStreamStartAt(...) later than .changeStreamEndAt(...) (or .changeStreamInclusiveEndAt(...)), e.g. computed timestamps where start is derived from a later checkpoint than end.

Common situations: Clock/timezone conversions producing inverted ranges; passing end from an older run while start is 'now'; mixing Instant and Timestamp conversions incorrectly.

Related errors


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