apache/pulsar · error · RestException
${legacy} legacy v4 client connection(s) still attached to $
Error message
${legacy} legacy v4 client connection(s) still attached to ${persistentBase}; disconnect them (or all clients are V5) before migrating, or retry with force=true What it means
HTTP 409 CONFLICT raised by the migration precheck when legacy protocol-v4 client connections are still attached to the source topic and force was not set. Migration to a scalable topic requires all consumers/producers to be on the v5 protocol, since v4 clients cannot talk to scalable topics.
Source
Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java:379
}
// For a partitioned topic, inspect per-partition stats rather than the aggregate:
// aggregation merges publishers by producer name into fresh stat objects that drop
// per-connection metadata, which would hide the V5-managed marker and make every
// V5 connection look like a legacy v4 one.
final CompletableFuture<Long> legacyCount = partitions > 0
? admin.topics().getPartitionedStatsAsync(persistentBase.toString(), true)
.thenApply(stats -> {
long count = 0;
for (TopicStats partitionStats : stats.getPartitions().values()) {
count += countLegacyConnections(partitionStats);
}
return count;
})
: admin.topics().getStatsAsync(persistentBase.toString())
.thenApply(ScalableTopics::countLegacyConnections);
return legacyCount.thenAccept(legacy -> {
if (legacy > 0) {
throw new RestException(Response.Status.CONFLICT,
legacy + " legacy v4 client connection(s) still attached to " + persistentBase
+ "; disconnect them (or all clients are V5) before migrating, "
+ "or retry with force=true");
}
});
}
private static long countLegacyConnections(TopicStats stats) {
long count = 0;
for (var publisher : stats.getPublishers()) {
if (!isV5Managed(publisher.getMetadata())) {
count++;
}
}
for (var subscription : stats.getSubscriptions().values()) {
for (var consumer : subscription.getConsumers()) {
if (!isV5Managed(consumer.getMetadata())) {
count++;View on GitHub (pinned to 820761864e)
Solutions
- Upgrade all connected clients to a protocol-v5-capable Pulsar client and let old connections drain, then retry migration.
- Identify which apps hold the legacy connections via topic stats and restart/upgrade them.
- Re-run the migration with force=true to bypass the precheck only if you accept v4 clients failing afterwards (they cannot consume from the scalable topic).
Example fix
// before await admin.scalableTopics().migrateToScalable(tenant, ns, topic); // 409: legacy clients // after await admin.scalableTopics().migrateToScalable(tenant, ns, topic, /*force*/ false); // ...or after upgrading clients, or deliberately: await admin.scalableTopics().migrateToScalable(tenant, ns, topic, /*force*/ true);
Defensive patterns
Strategy: retry
Validate before calling
const stats = await admin.topics().getStatsAsync(`persistent://${tenant}/${ns}/${topic}`);
const legacy = countLegacyConnections(stats);
if (legacy > 0) throw new Error(`${legacy} legacy v4 clients connected; upgrade them or use force=true`); Try / catch
try {
await admin.scalableTopics().migrateToScalable(tenant, ns, topic);
} catch (e) {
if (e.status === 409 && /legacy v4 client/.test(e.message)) {
await waitForClientsToUpgrade();
return admin.scalableTopics().migrateToScalable(tenant, ns, topic); // retry once
}
throw e;
} Prevention
- Inventory client library versions before migrating; require v5-protocol-capable clients.
- Monitor topic stats for legacy connections as a pre-migration gate.
- Only use force=true deliberately, after confirming v4 clients can be cut off.
When it happens
Trigger: Calling POST .../migrate without force=true while getStats-connection counting finds one or more v4 (legacy) client connections on the source topic or its partitions.
Common situations: Rolling out scalable-topic migration while old SDK versions (pre-v5 protocol) still run in production; forgotten background consumers (e.g. monitoring/backup apps) pinned to old client libs; canary environments sharing a topic with legacy services.
Related errors
- Topic is already scalable: ${scalableName}
- Topic does not exist: ${persistentBase}
- numInitialSegments must be >= 1
- Scalable topic not found: ${tn}
- Label name cannot be null or empty
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/0c8ca3231d9a2cd4.
Report an issue: GitHub.