apache/pulsar · warning · RestException

Get message ID by timestamp on a non-persistent topic is not

Error message

Get message ID by timestamp on a non-persistent topic is not allowed

What it means

The timestamp-to-message-ID lookup requires the target topic to be a PersistentTopic backed by a managed ledger; non-persistent topics have no durable entries to search. When getTopicReferenceAsync returns a topic that is not PersistentTopic, the broker logs 'Not supported operation of non-persistent topic' and throws 405 METHOD_NOT_ALLOWED.

Source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PersistentTopicsBase.java:3020

                    return CompletableFuture.completedFuture(null);
                } else {
                    return getPartitionedTopicMetadataAsync(topicName, authoritative, false)
                        .thenAccept(metadata -> {
                            if (metadata.partitions > 0) {
                                throw new RestException(Status.METHOD_NOT_ALLOWED,
                                    "Get message ID by timestamp on a partitioned topic is not allowed, "
                                        + "please try do it on specific topic partition");
                            }
                        });
                }
            }).thenCompose(__ -> validateTopicOwnershipAsync(topicName, authoritative))
            .thenCompose(__ -> getTopicReferenceAsync(topicName))
            .thenCompose(topic -> {
                if (!(topic instanceof PersistentTopic)) {
                    log.error()
                            .attr("topic", topicName)
                            .log("Not supported operation of non-persistent topic");
                    throw new RestException(Status.METHOD_NOT_ALLOWED,
                        "Get message ID by timestamp on a non-persistent topic is not allowed");
                }
                final PersistentTopic persistentTopic = (PersistentTopic) topic;
                final var compactionService = persistentTopic.getTopicCompactionService();

                return compactionService.getLastMessagePosition().thenCompose(messagePosition -> {
                    if (timestamp == messagePosition.publishTime()) {
                        return CompletableFuture.completedFuture(new MessageIdImpl(messagePosition.ledgerId(),
                                messagePosition.entryId(), topicName.getPartitionIndex()));
                    } else if (timestamp < messagePosition.publishTime()) {
                        return persistentTopic.getTopicCompactionService().findEntryByPublishTime(timestamp)
                                .thenApply(__ -> new MessageIdImpl(__.getLedgerId(), __.getEntryId(),
                                        topicName.getPartitionIndex()));
                    } else {
                        return findMessageIdByPublishTime(timestamp, persistentTopic.getManagedLedger());
                    }
                });
            });

View on GitHub (pinned to 820761864e)

Solutions

  1. Use a persistent topic for time-based message-ID lookup
  2. Check the topic domain in the URL (persistent:// vs non-persistent://) before calling
  3. Enable persistent bookkeeper storage for the topic if durability is required

Example fix

// before
String id = admin.topics().getMessageIdByTimestamp("non-persistent://public/default/my-topic", ts);
// after
String id = admin.topics().getMessageIdByTimestamp("persistent://public/default/my-topic", ts);
Defensive patterns

Strategy: validation

Validate before calling

if (!topic.startsWith("persistent://")) {
    throw new IllegalArgumentException("Timestamp lookup requires a persistent topic");
}

Type guard

boolean isPersistentTopic(String topic) { return topic.startsWith("persistent://"); }

Try / catch

try { ... getMessageIdByTimestamp(topic, ts); }
catch (PulsarAdminException e) { if (e.getStatusCode() == 405) { /* non-persistent or partitioned */ } }

Prevention

When it happens

Trigger: Calling getMessagedIdByTimestamp on a topic under the non-persistent:// domain, or on any topic reference that fails the instanceof PersistentTopic check.

Common situations: Reusing the same topic name string but switching the namespace/topic policy from persistent to non-persistent; tooling that hardcodes persistent:// but the cluster routes to a non-persistent topic of the same name.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/b0a40e0eca1de133. Report an issue: GitHub.