apache/pulsar · error · RestException
Cluster already exists
Error message
Cluster already exists
What it means
HTTP 409 (CONFLICT) thrown by the admin REST API when a client attempts to create a cluster whose name already exists in cluster metadata. The handler checks cluster existence asynchronously via clusterResources().getClusterAsync and throws before calling createClusterAsync. Cluster creation is idempotency-guarded: one name maps to one ClusterData record.
Source
Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/ClustersBase.java:184
)
)
) ClusterDataImpl clusterData) {
validateBothSuperuserAndClusterOperation(cluster, ClusterOperation.CREATE_CLUSTER)
.thenCompose(__ -> validatePoliciesReadOnlyAccessAsync())
.thenCompose(__ -> {
NamedEntity.checkName(cluster);
if (clusterData == null) {
throw new RestException(Status.BAD_REQUEST, "cluster data is required");
}
try {
clusterData.checkPropertiesIfPresent();
} catch (IllegalArgumentException ex) {
throw new RestException(Status.BAD_REQUEST, ex.getMessage());
}
return clusterResources().getClusterAsync(cluster);
}).thenCompose(clusterOpt -> {
if (clusterOpt.isPresent()) {
throw new RestException(Status.CONFLICT, "Cluster already exists");
}
return clusterResources().createClusterAsync(cluster, clusterData);
}).thenAccept(__ -> {
log.info().attr("cluster", cluster).log("Created cluster");
asyncResponse.resume(Response.ok().build());
}).exceptionally(ex -> {
log.error()
.attr("cluster", cluster)
.exception(ex)
.log("Failed to create cluster");
Throwable realCause = FutureUtil.unwrapCompletionException(ex);
if (realCause instanceof IllegalArgumentException) {
asyncResponse.resume(new RestException(PRECONDITION_FAILED,
"Cluster name is not valid"));
return null;
}
resumeAsyncResponseExceptionally(asyncResponse, ex);
return null;View on GitHub (pinned to 820761864e)
Solutions
- Use PUT /admin/v3/clusters/{cluster} (update/upsert semantics) instead of POST if you want create-or-update behavior.
- Check existence first with GET /admin/v3/clusters/{cluster}; if it returns 200 and config matches, treat the create as done.
- Delete the stale cluster with DELETE /admin/v3/clusters/{cluster} if it is leftover garbage and empty, then recreate.
- Pick a different, unique cluster name if the existing record is legitimate.
Example fix
// before
admin.clusters().createCluster(clusterName, clusterData); // 409 if exists
// after
try {
admin.clusters().createCluster(clusterName, clusterData);
} catch (PulsarAdminException.ConflictException e) {
admin.clusters().updateCluster(clusterName, clusterData); // upsert
} Defensive patterns
Strategy: validation
Validate before calling
boolean exists;
try {
admin.clusters().getCluster(clusterName);
exists = true;
} catch (PulsarAdminException.NotFoundException e) {
exists = false;
}
if (exists) {
admin.clusters().updateCluster(clusterName, clusterData); // upsert instead of create
} else {
admin.clusters().createCluster(clusterName, clusterData);
} Try / catch
try {
admin.clusters().createCluster(clusterName, clusterData);
} catch (PulsarAdminException.ConflictException e) {
// cluster already present: verify config then update
admin.clusters().updateCluster(clusterName, clusterData);
} Prevention
- Prefer PUT/update (upsert) semantics in provisioning code instead of POST create.
- Check GET /admin/v3/clusters/{cluster} before creating in idempotent automation.
- Serialize cluster provisioning (single tool/lock) to avoid concurrent create races.
When it happens
Trigger: POST /admin/v3/clusters/{cluster} where the cluster name is already present in the metadata store; concurrent createCluster calls racing on the same name; retrying a previously successful create request.
Common situations: Re-running bootstrap/provisioning scripts without checking existing clusters; two ops tools provisioning the same cluster concurrently; Terraform/Ansible re-apply after a partial failure where the cluster record was already written.
Related errors
- ResourceGroup already exists
- ${duplicateBrokers} already exists in ${domainName}
- Cannot delete non empty namespace
- %s is a non-partitioned topic. Instead of calling delete-par
- %s %s already exists
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/8609ac4b1f416f09.
Report an issue: GitHub.