{"record":{"id":"0c8ca3231d9a2cd4","repo":"apache/pulsar","slug":"legacy-legacy-v4-client-connection-s-still-att","errorCode":null,"errorMessage":"${legacy} legacy v4 client connection(s) still attached to ${persistentBase}; disconnect them (or all clients are V5) before migrating, or retry with force=true","messagePattern":"(.+?) legacy v4 client connection\\(s\\) still attached to (.+?); disconnect them \\(or all clients are V5\\) before migrating, or retry with force=true","errorType":"http","errorClass":"RestException","httpStatus":409,"severity":"error","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java","lineNumber":379,"sourceCode":"        }\n        // For a partitioned topic, inspect per-partition stats rather than the aggregate:\n        // aggregation merges publishers by producer name into fresh stat objects that drop\n        // per-connection metadata, which would hide the V5-managed marker and make every\n        // V5 connection look like a legacy v4 one.\n        final CompletableFuture<Long> legacyCount = partitions > 0\n                ? admin.topics().getPartitionedStatsAsync(persistentBase.toString(), true)\n                        .thenApply(stats -> {\n                            long count = 0;\n                            for (TopicStats partitionStats : stats.getPartitions().values()) {\n                                count += countLegacyConnections(partitionStats);\n                            }\n                            return count;\n                        })\n                : admin.topics().getStatsAsync(persistentBase.toString())\n                        .thenApply(ScalableTopics::countLegacyConnections);\n        return legacyCount.thenAccept(legacy -> {\n            if (legacy > 0) {\n                throw new RestException(Response.Status.CONFLICT,\n                        legacy + \" legacy v4 client connection(s) still attached to \" + persistentBase\n                                + \"; disconnect them (or all clients are V5) before migrating, \"\n                                + \"or retry with force=true\");\n            }\n        });\n    }\n\n    private static long countLegacyConnections(TopicStats stats) {\n        long count = 0;\n        for (var publisher : stats.getPublishers()) {\n            if (!isV5Managed(publisher.getMetadata())) {\n                count++;\n            }\n        }\n        for (var subscription : stats.getSubscriptions().values()) {\n            for (var consumer : subscription.getConsumers()) {\n                if (!isV5Managed(consumer.getMetadata())) {\n                    count++;","sourceCodeStart":361,"sourceCodeEnd":397,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java#L361-L397","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nawait admin.scalableTopics().migrateToScalable(tenant, ns, topic); // 409: legacy clients\n// after\nawait admin.scalableTopics().migrateToScalable(tenant, ns, topic, /*force*/ false);\n// ...or after upgrading clients, or deliberately:\nawait admin.scalableTopics().migrateToScalable(tenant, ns, topic, /*force*/ true);","handlingStrategy":"retry","validationCode":"const stats = await admin.topics().getStatsAsync(`persistent://${tenant}/${ns}/${topic}`);\nconst legacy = countLegacyConnections(stats);\nif (legacy > 0) throw new Error(`${legacy} legacy v4 clients connected; upgrade them or use force=true`);","typeGuard":null,"tryCatchPattern":"try {\n  await admin.scalableTopics().migrateToScalable(tenant, ns, topic);\n} catch (e) {\n  if (e.status === 409 && /legacy v4 client/.test(e.message)) {\n    await waitForClientsToUpgrade();\n    return admin.scalableTopics().migrateToScalable(tenant, ns, topic); // retry once\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["rest","conflict","client-compatibility","migration","scalable-topics","http-409"],"backgroundTag":"legacy-client-connected","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}