Data Multi-cloud

Configure Confluent Cloud Cluster Linking and Tiered Storage for Multi-Region Kafka

A payments company runs its order and settlement events through Kafka on Confluent Cloud in us-east-1. The mandate from the new platform lead is blunt: a regional outage cannot stop European settlement, and auditors want seven years of every settlement.completed event re-readable on demand — not in a cold archive nobody can replay, but as a live Kafka topic a consumer group can rewind into. Today the setup keeps seven days of retention on broker disk and replicates nothing across regions. This guide builds the fix: Cluster Linking to mirror topics from a us-east-1 source into a eu-west-1 destination with byte-for-byte offset fidelity and synced consumer offsets, and Tiered Storage so both clusters offload old segments to object storage and keep months-to-years of history without paying for hot broker disk.

Two features carry the design, and they solve orthogonal problems. Cluster Linking is geo-replication: a managed, destination-initiated link that pulls a configured set of topics into read-only mirror topics preserving partition count, keys, and exact offsets — the foundation for active-passive DR, regional read locality, and zero-downtime migration. Tiered Storage is the retention economics: every Dedicated cluster offloads closed segments older than a small hotset to managed cloud object storage (S3/GCS/Azure Blob), so a topic holds years while brokers hold only hours. Together they let you say “every settlement event, in two regions, for seven years, re-readable as a live topic” — and mean it, at a bill you can defend.

This is the implementation playbook, written for someone who will run it. You will stand up two clusters, wire a link, create mirror topics, tune retention and hotset for cost, fail over to the second region with consumer offsets intact, and bolt on the operating model — Terraform, Vault, Okta/Entra SSO, the Metrics API into Datadog/Dynatrace, and ServiceNow. Every operation comes with both the real confluent CLI command and the Terraform that makes it declarative, and the cluster types, link settings, ACL grants, failure modes, and cost levers are laid out as scannable tables. Read the prose once, then keep the tables open while you build.

What problem this solves

Single-region Kafka is a single point of failure with a short memory. The two pains it creates are independent, and most teams hit them in this order.

The first pain is resilience. A regional outage — control-plane degradation, a cascading zone failure, or a deliberate evacuation — takes your entire event backbone offline: producers can’t write, consumers can’t read, downstream systems stall. If your settlement pipeline lives only in us-east-1, a us-east-1 incident is a settlement outage, full stop. The classic answer was MirrorMaker 2 on a Connect cluster you operate — a second piece of infrastructure that fails on its own schedule and mangles offsets unless you run the offset-sync connectors perfectly. Cluster Linking removes that burden: the link is managed, offsets are preserved natively, and there is no Connect cluster to run.

The second pain is retention economics. Kafka keeps data on broker local disk by default, and broker disk on a Dedicated cluster is expensive, finite, and tied to your CKU (Confluent Unit for Kafka) sizing. Seven days is routine; seven years on broker disk would mean enormous, mostly-idle storage at hot-disk prices every hour. So teams compromise: short retention plus a fragile sink connector dumping old events into a lake nobody can replay as Kafka — and when an audit needs the history, the export turns out to have dropped fields or lost ordering. Tiered Storage fixes this directly: closed segments move to object storage (cents per GB), the topic still presents a continuous offset space, and a consumer reading an old offset transparently fetches from the cold tier — slightly slower, but it is Kafka, with the original bytes and ordering.

Who hits this: any team running Kafka for events that matter across regions or over long horizons — payments and settlement (the canonical case), order/inventory streams feeding multiple geographies, IoT and clickstream platforms retaining raw events for ML backfills, and regulated workloads (finance, healthcare, telco) with multi-year audit-replay needs. Without these features a regional incident becomes a business outage and an audit request becomes an archaeology project. The whole field, framed before the deep dive:

Concern Single-region, short-retention Kafka With Cluster Linking With Tiered Storage Combined
Regional outage Total event-backbone outage Promote mirror in second region; producers fail over No help (single region still down) DR with offsets preserved
Long retention (years) Impossible at hot-disk cost No help by itself Cheap cold tier; topic stays replayable Years of history, in both regions
Audit replay Archaeology from a lossy lake export Replay from the mirror region too Replay original bytes from cold tier Replay either region, any offset
Migration to new cluster Dual-write or downtime Link + mirror + cutover, no downtime Zero-downtime migration
Read locality (EU consumers) Cross-region reads, high latency Read the local mirror Local low-latency reads + DR
Operator burden Run MirrorMaker/Connect yourself Managed link, no Connect cluster Managed offload, no tooling Both fully managed

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand Kafka fundamentals: topics and partitions, per-partition ordering, what an offset is (a record’s monotonic position within a partition), what a consumer group is and how it commits offsets, and the producer/consumer model. If those are fuzzy, read Event Hubs Fundamentals: Partitions, Consumer Groups, Checkpoints and Offsets for Beginners first — the partition/offset model is identical across Kafka-compatible systems, and the whole DR story rests on why offset preservation is the hard part.

You should also be comfortable with the operating tools: Terraform ≥ 1.6 with the confluentinc/confluent provider, the confluent CLI v3.x+, and the idea that secrets live in HashiCorp Vault, not in terraform.tfvars. Disaster-recovery vocabulary helps — RTO (how fast you recover) and RPO (how much data you can lose) frame every choice here; High Availability vs Disaster Recovery: RTO and RPO Explained is the primer, and BCDR Foundations on Azure: Making Sense of RTO, RPO, and the Resilience Spectrum maps the spectrum.

This sits in the streaming-platform track. Cluster Linking is the cross-region transport; the broader question of which replication topology and consistency model to pick is Multi-Region Data: Choosing Replication and Consistency Without Losing Writes. Once data flows in two regions you feed it into analytics — Real-Time Analytics with Databricks and Confluent Kafka on AWS — or use Kafka as the CDC backbone, as in Real-Time CDC with Debezium and Kafka. A map of who owns what during this build:

Layer What lives here Who usually owns it What it can break
Source cluster (us-east-1) Writeable topics, producer ACLs, tiering config Platform / streaming team Producer writes; tiering cost
Cluster Link Managed link, mirror state, offset sync Platform team Replication lag, failover correctness
Destination cluster (eu-west-1) Mirror topics, EU consumer ACLs Platform team EU reads; DR readiness
Schema Registry / Stream Governance Subjects, compatibility, Schema Linking Data governance Deserialization in the destination
Networking (public / PrivateLink / peering) Bootstrap reachability between regions Network team Link connectivity
Identity (Okta/Entra + RBAC) Human SSO, role bindings, service accounts IAM team Who can touch the clusters
Secrets (Vault) API key/secret leases Security / platform Credential rotation, leaks
CI/CD (Terraform + Argo CD) Declared topology, drift control Platform team Config drift, accidental deletes

Core concepts

Six mental models make every later step obvious. Read these once; the deep sections assume them.

Cluster Linking is destination-initiated and managed. You create the link object on the destination cluster, point it at the source’s bootstrap, and the link pulls topics from source to destination as mirror topics. There is no Connect cluster or MirrorMaker process to scale or patch — it is a first-class managed component of the destination. “Destination-initiated, source-pulled” explains every CLI flag: you run link create and mirror create on the destination, never the source.

A mirror topic is a read-only, offset-faithful copy. Unlike MirrorMaker 2 (which republishes records with new offsets, then runs separate offset-sync to translate), a mirror preserves the exact offsets, partition count, keys, and topic settings — offset 4,418,002 on partition 7 of the source is offset 4,418,002 on partition 7 of the mirror. That fidelity is why consumer failover is clean: a group’s committed offset means the same thing on both sides. A mirror is read-only until you promote or failover it, which severs the link and makes it a normal writeable topic.

Offset sync makes failover correct, not just possible. Mirroring records is necessary but not sufficient. If a group in us-east-1 is at offset 1,000,000 and the region dies, the failed-over group must resume at 1,000,001, not restart from the beginning (re-processing millions of events) or from latest (silently skipping them). consumer.offset.sync.enable=true copies committed group offsets source→destination continuously, so the destination knows where each group was. It is set at link creation and awkward to retrofit — decide it up front.

Tiered Storage decouples retention from disk. On a Dedicated cluster, every topic’s closed segments offload to object storage. Two per-topic levers control it: retention.ms (how long data is kept anywhere) and confluent.tier.local.hotset.ms (how much recent data stays on broker disk). Set retention to seven years and the hotset to six hours, and you keep seven years total while brokers hold six — the object store holds the rest at a fraction of the cost. A read older than the hotset transparently fetches from object storage with modestly higher latency. This is what makes “years of replayable history” economically real.

Hot reads and cold reads differ. Reads from the hotset (broker disk, page cache) are normal low-latency Kafka. Reads of older offsets fetch from object storage — higher first-byte latency, and a large backfill drives tiered fetch throughput and can momentarily pressure the cluster. That suits replay and audit (bursty, latency-tolerant), but a heavy backfill is an operation to plan, not a casual --from-beginning against a multi-terabyte topic in business hours.

Cluster type gates the features. Cluster Linking and per-topic Tiered Storage tuning are not on every tier. Basic and Standard are multi-tenant, cheaper, limited; Dedicated gives isolated CKU capacity, Cluster Linking, full Tiered Storage control, private networking, and the scale this design needs. Choosing the tier is the first real decision, and getting it wrong means re-provisioning. The vocabulary, side by side:

Concept One-line definition Where it lives Why it matters here
CKU Confluent Unit for Kafka — the capacity unit of a Dedicated cluster Per Dedicated cluster Sizes throughput, partitions, connections; drives base cost
Cluster Link Managed, destination-initiated replication pipe On the destination cluster The geo-replication mechanism
Mirror topic Read-only, offset-faithful copy over a link On the destination cluster What EU consumers read; promoted on failover
Offset sync Copies committed consumer-group offsets source→dest Link config Makes failover resume at the right place
Hotset Recent data kept on broker local disk Per-topic config Caps expensive hot storage
Tiered Storage Offload of closed segments to object storage Per Dedicated cluster Cheap long retention
Promote / Failover Convert a mirror topic to writeable Mirror operation The DR / migration cutover
Schema Linking Replicate Schema Registry subjects across clusters Stream Governance Lets the destination deserialize
Service account Non-human identity holding API keys Org/environment Scoped principals for producer/consumer/link
ACL / RBAC Authorization on topics, groups, cluster Per cluster / org Least-privilege access control

Cluster types: Basic vs Standard vs Enterprise vs Dedicated

The first decision is which cluster type to provision, because Cluster Linking and full Tiered Storage control are Dedicated-class capabilities — attempting them on a Basic or Standard cluster fails at create time, and choosing wrong means re-provisioning and migrating. The comparison that matters for this workload (multi-region, long retention, production scale):

Capability Basic Standard Enterprise Dedicated
Tenancy Multi-tenant Multi-tenant Multi-tenant (elastic) Single-tenant (isolated)
Capacity model Usage-based, capped Usage-based, higher caps Elastic CKU-like (eCKU) Provisioned CKUs
Cluster Linking No Source/Destination supported (with limits) Supported Full support (any direction)
Tiered Storage Managed, not tunable per topic the same way Managed Managed Full per-topic retention/hotset control
Dedicated throughput / scale Low Medium High (elastic) Highest, predictable
Private networking (PrivateLink / peering) No No Yes (some) Yes (PrivateLink, peering, Transit GW)
Self-managed encryption keys (BYOK) No No Limited Yes
Uptime SLA Lower Standard High Highest (multi-zone)
Best for Dev, small workloads Production, moderate scale Production needing elasticity without CKU planning Mission-critical multi-region, long retention

As a decision table — match your requirement to the smallest type that satisfies it:

If you need… Smallest cluster type Why
A throwaway dev cluster Basic Cheapest; no link/DR ambitions
Production at moderate scale, no cross-region Standard Cost-effective; Cluster Linking available with limits
Elastic scale without provisioning CKUs Enterprise eCKU autoscaling; private networking
Multi-region replication with predictable, isolated capacity Dedicated Full Cluster Linking + Tiered Storage control + CKUs
BYOK encryption / PrivateLink / multi-zone SLA Dedicated Single-tenant features the others lack
Years of replayable retention you actively tune Dedicated Per-topic hotset.ms control

For the payments design — two regions, seven-year retention tuned per topic, private networking, and a hard SLA — the answer is Dedicated on both ends. A CKU bundles throughput, partition, and connection capacity; the minimum multi-zone Dedicated cluster is 2 CKUs (single-zone can start at 1, but you do not run production payments single-zone). Sizing is covered in Cost & sizing below.

Authenticate, pin the environment, and stage credentials in Vault

Log in (SSO-backed, so the browser hands you off to Okta/Entra for MFA), then pin the environment so every later command is unambiguous. Acting on the wrong environment is a classic, expensive mistake when you have prod and non-prod side by side.

confluent login --save                          # SSO -> Okta/Entra, MFA, token cached
confluent environment list
confluent environment use env-payments-prod      # pin so we never act on the wrong env
confluent kafka cluster list                      # see what already exists in this env

Create a bootstrap Cloud API key and store it in Vault immediately — never leave it in shell history or a CI log. It is an org-level credential used only to provision; runtime principals get their own scoped keys. CI reads it from Vault with a short-lived token, so the static secret is fetched at job time, not baked into a pipeline.

confluent api-key create --resource cloud --description "tf-bootstrap-payments"
# -> prints API Key + Secret ONCE. Pipe straight into Vault; do not echo to a file or commit it.
vault kv put secret/confluent/payments-bootstrap \
  api_key="<KEY>" api_secret="<SECRET>"

In Terraform, the provider reads those values from Vault rather than from variables on disk:

terraform {
  required_providers {
    confluent = { source = "confluentinc/confluent", version = "~> 2.0" }
    vault     = { source = "hashicorp/vault" }
  }
  required_version = ">= 1.6"
}

data "vault_kv_secret_v2" "cc_bootstrap" {
  mount = "secret"
  name  = "confluent/payments-bootstrap"
}

provider "confluent" {
  cloud_api_key    = data.vault_kv_secret_v2.cc_bootstrap.data["api_key"]
  cloud_api_secret = data.vault_kv_secret_v2.cc_bootstrap.data["api_secret"]
}

The credential model for the whole build. Each layer gets a different identity, and each secret lands in Vault under its own path so a leak is a scoped, rotatable blast radius:

Credential Scope Used by Stored where Rotation impact
Cloud API key Org / environment (OrganizationAdmin/EnvironmentAdmin) Terraform provisioning only secret/confluent/payments-bootstrap Re-roll; no runtime impact
Producer key Source cluster, WRITE on topic prefix EAST producers secret/confluent/payments-producer Producers only
Consumer key Destination cluster, READ on topic + group EU consumers secret/confluent/payments-consumer Consumers only
Link principal key Source cluster, READ/DESCRIBE The Cluster Link secret/confluent/payments-link Link only; re-create config
Schema Registry key Stream Governance API Producers/consumers (serde) secret/confluent/payments-sr Serde clients only

Declare both Dedicated clusters in Terraform

Cluster Linking needs Dedicated clusters. Declare both regions and the environment in one config so the topology is reviewable, reproducible, and drift-controlled — the artifact CI runs and Argo CD reconciles. MULTI_ZONE availability is non-negotiable for production: it spreads replicas across zones so a single-zone failure doesn’t take the cluster down — the in-region HA that complements the cross-region DR the link provides.

resource "confluent_environment" "payments" {
  display_name = "payments-prod"
  stream_governance { package = "ADVANCED" }   # Schema Registry for both regions
}

resource "confluent_kafka_cluster" "source_us" {
  display_name = "payments-source-use1"
  availability = "MULTI_ZONE"
  cloud        = "AWS"
  region       = "us-east-1"
  dedicated { cku = 2 }
  environment { id = confluent_environment.payments.id }
}

resource "confluent_kafka_cluster" "dest_eu" {
  display_name = "payments-dest-euw1"
  availability = "MULTI_ZONE"
  cloud        = "AWS"
  region       = "eu-west-1"
  dedicated { cku = 2 }
  environment { id = confluent_environment.payments.id }
}

Apply through CI, not a laptop, so the state file is authoritative and the change is reviewed:

terraform init && terraform plan -out tfplan
terraform apply tfplan          # provisioning Dedicated CKUs takes several minutes

The cluster resource arguments, what they control, and the gotcha for each:

Argument What it controls Values Gotcha
availability Zone spread SINGLE_ZONE / MULTI_ZONE Multi-zone is mandatory for prod SLA; can’t change post-create easily
cloud Cloud provider AWS / AZURE / GCP Source and dest can differ, but cross-cloud mirroring has egress cost/latency
region Cloud region Provider region string Must be a region Confluent supports for that cloud
dedicated.cku Provisioned capacity Integer (≥2 for multi-zone) Under-size and you throttle; over-size and you overpay. Scale via Terraform later
environment.id Owning environment Env resource id Both clusters in the same environment for linking simplicity
display_name Human name String Make it encode role+region (source-use1) so ops is unambiguous

A note on networking, because the link must reach the source’s bootstrap. With public clusters the destination dials the source over the public internet on :9092 (SASL_SSL). For production you usually want PrivateLink, VPC/VNet peering, or Transit Gateway so replication never traverses the public internet — that adds a confluent_network and access-point resources, and the link’s bootstrap.servers points at the private endpoint. Decide the network model before the link, because it changes the bootstrap address the link config carries.

Create service accounts and scoped, Vault-backed credentials

Give producers, consumers, and the link their own identities — never reuse the bootstrap key for runtime. Each service account gets a cluster-scoped API key whose secret lands in Vault under a per-service path, so a leaked consumer key rotates without touching producers.

# identities
confluent iam service-account create sa-payments-producer --description "EAST producers"
confluent iam service-account create sa-payments-consumer --description "EU consumers"
confluent iam service-account create sa-payments-link     --description "Cluster Link principal"

# scoped key for the producer, against the SOURCE cluster
confluent api-key create --service-account sa-payments-producer \
  --resource lkc-source-use1
vault kv put secret/confluent/payments-producer api_key="<KEY>" api_secret="<SECRET>"

# scoped key for the link principal, also against the SOURCE (it reads the source)
confluent api-key create --service-account sa-payments-link \
  --resource lkc-source-use1
vault kv put secret/confluent/payments-link api_key="<KEY>" api_secret="<SECRET>"

Then grant least-privilege ACLs (full matrix below): producers WRITE/DESCRIBE the source topic prefix, the link principal READ/DESCRIBE the source topics plus DESCRIBE the cluster, consumers READ the destination mirrors and their group.

# SOURCE: producers may only WRITE the payments topics
confluent kafka acl create --cluster lkc-source-use1 \
  --allow --service-account sa-payments-producer \
  --operations WRITE,DESCRIBE --topic settlement --prefix

# SOURCE: the link principal may READ the topics it mirrors + DESCRIBE the cluster
confluent kafka acl create --cluster lkc-source-use1 \
  --allow --service-account sa-payments-link \
  --operations READ,DESCRIBE --topic settlement --prefix
confluent kafka acl create --cluster lkc-source-use1 \
  --allow --service-account sa-payments-link \
  --operations DESCRIBE --cluster-scope

# DESTINATION: EU consumers may READ the mirror topics + their group
confluent kafka acl create --cluster lkc-dest-euw1 \
  --allow --service-account sa-payments-consumer \
  --operations READ,DESCRIBE --topic settlement --prefix
confluent kafka acl create --cluster lkc-dest-euw1 \
  --allow --service-account sa-payments-consumer \
  --operations READ --consumer-group eu-settlement --prefix

The complete least-privilege grant matrix. A distinct link principal lets the audit trail show what the link copied versus what an app wrote:

Principal Cluster Resource Operations Why
sa-payments-producer Source Topic settlement* WRITE, DESCRIBE Producers append events; nothing else
sa-payments-link Source Topic settlement* READ, DESCRIBE Link reads source records to mirror
sa-payments-link Source Cluster DESCRIBE Link discovers topic/partition metadata
sa-payments-consumer Destination Topic settlement* READ, DESCRIBE EU consumers read the mirror
sa-payments-consumer Destination Group eu-settlement* READ Consumers commit group offsets
(none) Destination Mirror topic WRITE Never granted — mirrors are read-only

In Terraform the same grants are declarative via confluent_kafka_acl (or confluent_role_binding for RBAC), keyed to the service-account resources — a reviewer sees who can do what, and a removed block revokes access on the next apply.

Turn on Tiered Storage retention on the source topics

Dedicated clusters already tier closed segments to object storage; what you control per topic is how long data is retained (retention.ms) and the hotset that stays on broker disk (confluent.tier.local.hotset.ms). Create the topics with long retention and a modest hotset so brokers stay lean while the topic holds years.

# 7-year retention, but only ~last 6h of segments kept on broker disk (rest in object store)
confluent kafka topic create settlement.completed \
  --cluster lkc-source-use1 \
  --partitions 12 \
  --config retention.ms=220752000000 \
  --config confluent.tier.local.hotset.ms=21600000 \
  --config cleanup.policy=delete

confluent kafka topic create order.events \
  --cluster lkc-source-use1 --partitions 12 \
  --config retention.ms=220752000000 \
  --config confluent.tier.local.hotset.ms=21600000

The same in Terraform, declarative and drift-controlled:

resource "confluent_kafka_topic" "settlement" {
  kafka_cluster { id = confluent_kafka_cluster.source_us.id }
  topic_name       = "settlement.completed"
  partitions_count = 12
  config = {
    "retention.ms"                   = "220752000000" # ~7 years
    "confluent.tier.local.hotset.ms" = "21600000"     # ~6h hot on disk
    "cleanup.policy"                 = "delete"
  }
  rest_endpoint = confluent_kafka_cluster.source_us.rest_endpoint
  credentials {
    key    = data.vault_kv_secret_v2.cc_admin.data["api_key"]
    secret = data.vault_kv_secret_v2.cc_admin.data["api_secret"]
  }
}

confluent.tier.local.hotset.ms is the lever that makes long retention cheap: it caps disk-resident data regardless of retention.ms, while reads of older offsets transparently fetch from object storage — the right trade for replay and audit. The Tiered Storage config reference you will return to:

Config What it controls Typical value (this design) Default behaviour Gotcha
retention.ms How long data is kept before deletion 220752000000 (~7y) 7 days This sets total retention; cost lives in the cold tier
retention.bytes Size cap per partition (alternative to time) unset (time-based) unset If set, the smaller of bytes/time wins
confluent.tier.local.hotset.ms Recent data kept on local disk (by time) 21600000 (6h) platform default Smaller = cheaper disk, more cold reads
confluent.tier.local.hotset.bytes Recent data kept on local disk (by size) optional platform default Use bytes if throughput is spiky
cleanup.policy delete vs compact delete delete Compacted topics tier differently; events use delete
segment.ms / segment.bytes When a segment closes (becomes tier-eligible) defaults platform default Only closed segments tier; tiny segments tier sooner

The crucial distinction between the two storage tiers — confusing them is how people mis-budget or panic about latency:

Aspect Hotset (local broker disk) Cold tier (object storage)
Holds Last hotset.ms of data Everything older, up to retention.ms
Read latency Low (page cache / local disk) Higher first-byte (object fetch)
Cost per GB High (broker storage, CKU-tied) Low (S3/GCS/Blob managed by Confluent)
Used by Live consumers near the tail Replay, audit, backfill, DR catch-up
Tuned by hotset.ms / hotset.bytes implicit (everything not in hotset)
Failure of a big read here Normal fast read Tiered-fetch throughput pressure; plan backfills

Create the Cluster Link from the destination

Cluster Linking is destination-initiated: you create the link on the eu-west-1 cluster and point it at us-east-1, and it runs as managed infrastructure. First write a config file giving the link the source’s bootstrap and a source-scoped key (from Vault):

# link-config.properties
link.mode=DESTINATION
connection.mode=OUTBOUND
bootstrap.servers=<SOURCE_BOOTSTRAP>:9092
security.protocol=SASL_SSL
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule \
  required username="<LINK_KEY>" password="<LINK_SECRET>";
consumer.offset.sync.enable=true
consumer.offset.sync.ms=30000
acl.sync.enable=true
auto.create.mirror.topics.enable=false

Then create the link, naming source and destination cluster IDs:

confluent kafka link create payments-east-to-west \
  --cluster lkc-dest-euw1 \
  --source-cluster lkc-source-use1 \
  --source-bootstrap-server <SOURCE_BOOTSTRAP>:9092 \
  --config-file link-config.properties

confluent kafka link list --cluster lkc-dest-euw1     # expect the link in 'ACTIVE'
confluent kafka link describe payments-east-to-west --cluster lkc-dest-euw1

The same link in Terraform, source-scoped credential from Vault:

resource "confluent_cluster_link" "east_to_west" {
  link_name = "payments-east-to-west"
  link_mode = "DESTINATION"
  connection_mode = "OUTBOUND"

  source_kafka_cluster {
    id            = confluent_kafka_cluster.source_us.id
    bootstrap_endpoint = confluent_kafka_cluster.source_us.bootstrap_endpoint
    credentials {
      key    = data.vault_kv_secret_v2.cc_link.data["api_key"]
      secret = data.vault_kv_secret_v2.cc_link.data["api_secret"]
    }
  }
  destination_kafka_cluster {
    id            = confluent_kafka_cluster.dest_eu.id
    rest_endpoint = confluent_kafka_cluster.dest_eu.rest_endpoint
    credentials {
      key    = data.vault_kv_secret_v2.cc_admin.data["api_key"]
      secret = data.vault_kv_secret_v2.cc_admin.data["api_secret"]
    }
  }
  config = {
    "consumer.offset.sync.enable"      = "true"
    "consumer.offset.sync.ms"          = "30000"
    "acl.sync.enable"                  = "true"
    "auto.create.mirror.topics.enable" = "false"
  }
}

Every link setting that matters, what it does, and why this design sets it — these levers determine whether your failover is clean:

Link setting What it does This design Why
link.mode DESTINATION vs SOURCE vs BIDIRECTIONAL DESTINATION Standard one-way DR/migration topology
connection.mode OUTBOUND vs INBOUND OUTBOUND Destination dials out to the source bootstrap
bootstrap.servers Source cluster endpoint source :9092 (or private) Where the link pulls from
security.protocol Transport security SASL_SSL Encrypted, authenticated link traffic
sasl.mechanism Auth mechanism PLAIN (API key/secret) Confluent Cloud API-key auth
consumer.offset.sync.enable Copy committed group offsets src→dst true The setting that makes failover resume correctly
consumer.offset.sync.ms How often offsets sync 30000 (30s) Bounds RPO on offsets; lower = fresher, more overhead
acl.sync.enable Replicate source ACLs to destination true Consumers keep their grants after failover
auto.create.mirror.topics.enable Auto-mirror new source topics false Explicit control over what (and what cost) you replicate
consumer.group.prefix.enable Optional group-name prefixing unset Leave off when groups should map 1:1 across regions

Again: consumer.offset.sync.enable=true is what makes failover clean, it is set at link creation, and it cannot be retrofitted cleanly — which is why the link decisions belong at the start of the design.

Create mirror topics on the destination

Mirror topics are read-only copies that inherit the source’s partition count and exact offsets. Create one per topic you want in Europe, over the link. They do not exist until you create them — auto.create.mirror.topics.enable=false means nothing is replicated implicitly, exactly the control you want over cross-region cost.

confluent kafka mirror create settlement.completed \
  --cluster lkc-dest-euw1 --link payments-east-to-west

confluent kafka mirror create order.events \
  --cluster lkc-dest-euw1 --link payments-east-to-west

confluent kafka mirror list --cluster lkc-dest-euw1 --link payments-east-to-west
confluent kafka mirror describe settlement.completed \
  --cluster lkc-dest-euw1 --link payments-east-to-west

In Terraform, each mirror is a resource on the link:

resource "confluent_kafka_mirror_topic" "settlement" {
  source_kafka_topic { topic_name = confluent_kafka_topic.settlement.topic_name }
  cluster_link { link_name = confluent_cluster_link.east_to_west.link_name }
  kafka_cluster {
    id            = confluent_kafka_cluster.dest_eu.id
    rest_endpoint = confluent_kafka_cluster.dest_eu.rest_endpoint
    credentials {
      key    = data.vault_kv_secret_v2.cc_admin.data["api_key"]
      secret = data.vault_kv_secret_v2.cc_admin.data["api_secret"]
    }
  }
}

Set the destination topics’ own Tiered Storage retention so Europe keeps the same seven-year horizon — mirror topics tier independently, so the destination needs its own retention/hotset config or it quietly falls back to a ~week default:

confluent kafka topic update settlement.completed --cluster lkc-dest-euw1 \
  --config retention.ms=220752000000 \
  --config confluent.tier.local.hotset.ms=21600000

A mirror topic moves through a small lifecycle of states, and the state dictates which operation is legal and what failover will do:

Mirror state Meaning What you can do What it means for failover
ACTIVE Linked, mirroring, read-only pause, promote, failover Healthy; lag should be low
PAUSED Mirroring stopped, still read-only resume, promote, failover Data is stale by the pause duration
PROMOTED Link severed gracefully, now writeable Produce normally Clean cutover (source was reachable)
FAILED_OVER Link severed forcibly, now writeable Produce normally DR cutover (source unreachable)
STOPPED Mirror removed Topic no longer replicates
LINK_FAILED Underlying link unhealthy Inspect link; fix connectivity Replication paused; lag growing

Now eu-west-1 consumers read settlement.completed locally while writes still happen only in us-east-1 — read locality and a warm DR copy from one managed pipe.

Schema Linking: making the destination able to deserialize

A subtle production trap: the mirror carries the records, but a consumer in eu-west-1 using Avro/Protobuf/JSON-Schema needs the schemas they reference. Each record embeds a schema ID the consumer resolves against a Schema Registry; if the destination’s registry lacks those schemas, EU consumers fail to deserialize — the data is there, but unreadable. Decide the schema strategy up front, alongside the link.

The options:

Strategy How it works Pros Cons / when to avoid
Shared Schema Registry One Stream Governance package both clusters use Simplest; schemas exist once SR availability tied to one region; not ideal for hard DR isolation
Schema Linking Replicate subjects from source SR to destination SR Destination has its own SR copy; DR-clean One more link to operate and monitor
Manual export/import Dump subjects, import on the other side No live dependency Drifts; humans forget; not for prod
No schema strategy (the mistake) EU consumers get deserialization errors

With Stream Governance ADVANCED (declared on the environment earlier), set up a schema exporter so subjects flow to the destination SR and stay in step with the source:

# Create a schema exporter on the source SR that pushes subjects to the destination SR
confluent schema-registry exporter create payments-schema-link \
  --subjects ":*:" \
  --context-type AUTO \
  --config-file schema-exporter.properties

confluent schema-registry exporter status payments-schema-link    # expect RUNNING

Destination consumers then resolve schema IDs locally. Keep compatibility mode (BACKWARD is the common default) consistent across both registries — a mismatch turns a routine producer schema bump into a destination-side break.

Consumer failover and the migration cutover

This is the payoff. Two operations look similar but mean different things, and choosing wrong loses data or causes split-brain.

Promote is a graceful cutover used when the source is still reachable — a planned migration or DR drill. It stops mirroring only after catching the destination fully up to the source, then makes the mirror writeable, so a promote is lossless.

Failover is a forced cutover used when the source is gone — a real outage where you cannot reach us-east-1. It severs the link immediately and makes the mirror writeable using whatever it had already replicated; anything not yet mirrored (the replication lag at the moment of failure) is your RPO — the data you may lose. This is why you alert on lag: lag is your potential data loss.

# PLANNED migration / drill — source reachable, lossless:
confluent kafka mirror promote settlement.completed \
  --cluster lkc-dest-euw1 --link payments-east-to-west

# REAL outage — source unreachable, accept lag-as-RPO:
confluent kafka mirror failover settlement.completed \
  --cluster lkc-dest-euw1 --link payments-east-to-west

Promote vs failover, decided:

Dimension Promote (graceful) Failover (forced)
When to use Planned migration / DR drill Real source outage
Source reachability Required (reachable) Not required (source gone)
Data loss None (catches up first) Up to the replication lag (your RPO)
Speed Slower (waits for catch-up) Immediate
Reversibility Plan a reverse link to fail back Plan a reverse link to fail back
Change-management Standard change ticket Incident-driven, post-hoc ticket

After either cutover, producers fail over — repointed to the now-writeable eu-west-1 topic — and because offsets were preserved and synced, consumers resume at the right place. The full failover runbook:

# Step Command / action Validation
1 Declare the incident Open ServiceNow incident; page on-call Ticket number assigned
2 Confirm source is truly down confluent kafka cluster describe lkc-source-use1 (unreachable) Not a transient blip
3 Check current lag (your RPO) Metrics API cluster_link_mirror_topic_offset_lag Quantify potential loss
4 Failover the mirror topic(s) confluent kafka mirror failover ... State → FAILED_OVER
5 Confirm topic is writeable confluent kafka topic produce settlement.completed --cluster lkc-dest-euw1 Produce succeeds
6 Repoint producers Update producer bootstrap → eu-west-1 Producers writing in EU
7 Confirm consumers resumed confluent kafka consumer-group describe eu-settlement --cluster lkc-dest-euw1 Offsets continue, not reset
8 Record the change Update ServiceNow with timeline + RPO Audit trail complete
9 Plan fail-back Build reverse link west-to-east once us-east-1 returns Documented before the next drill

For a migration (decommissioning the old cluster, not an outage) the flow is the same shape with promote instead of failover, in a maintenance window, producers cut over only after the promote confirms the destination is caught up. That is the zero-downtime cluster move: link, mirror, catch up, promote, repoint — no dual-write window, no offset translation.

Monitoring replication and tiered storage with the Metrics API

Replication you don’t watch is replication you don’t trust. Confluent exposes a Prometheus-compatible Metrics API you scrape (or pull into Datadog/Dynatrace via their Confluent integrations). The single most important metric here is cluster_link_mirror_topic_offset_lag — the per-mirror gap between source and destination — because on a forced failover that lag is your data loss. Wire it before you need it.

Pull the metrics over HTTP with a Metrics-API key (read-only telemetry scope, stored in Vault like every credential):

# Query mirror-topic offset lag over the last hour for the destination cluster
curl -s -u "$METRICS_KEY:$METRICS_SECRET" \
  "https://api.telemetry.confluent.cloud/v2/metrics/cloud/query" \
  -H 'Content-Type: application/json' -d '{
    "aggregations":[{"metric":"io.confluent.kafka.server/cluster_link_mirror_topic_offset_lag"}],
    "filter":{"field":"resource.kafka.id","op":"EQ","value":"lkc-dest-euw1"},
    "granularity":"PT1M","intervals":["PT1H/now"]
  }'

The metrics that tell you whether this system is healthy, and what a bad value indicates:

Metric What it measures Healthy A bad value means Surfaces in
cluster_link_mirror_topic_offset_lag Per-mirror gap source→dest Near 0, flat Link can’t keep up; RPO eroding Datadog/Dynatrace
Cluster link state (ACTIVE) Link health ACTIVE Replication paused/degraded link describe; Metrics API
received_bytes / sent_bytes Throughput per cluster Tracks ingress Saturation → throttling, lag Metrics API
retained_bytes Total bytes retained (hot+cold) Grows toward 7y plateau Unexpected growth → cost surprise Billing + Metrics
Tiered fetch bytes/rate Reads served from object store Low at steady state Spike = large backfill underway Metrics API
partition_count Partitions per cluster Within CKU ceiling Near ceiling → can’t add topics Metrics API
consumer_lag_offsets Consumer group lag Within SLO Consumers falling behind Metrics + group describe
active_connection_count Client connections Within CKU limit Near limit → connection refusals Metrics API

Turn those into alerts — on the leading indicators (lag, link state) so you act before consumers or auditors notice:

Alert on Threshold (starting point) Severity Why it’s leading Action
Mirror offset lag > 100k records, or > 60s, sustained 5 min Page Lag is your forced-failover RPO Check link/CKUs; reduce replicated topics
Link not ACTIVE Any state ≠ ACTIVE for 2 min Page Replication is paused Inspect link; fix connectivity/key
Cluster throughput > 80% of CKU ingress for 10 min Warn Predicts lag and throttling Scale CKUs via Terraform
Partition count > 85% of CKU partition ceiling Warn Can’t create new topics soon Add CKUs or consolidate topics
Tiered fetch spike Sustained high outside a planned window Warn Unplanned heavy backfill / runaway consumer Identify the consumer; throttle/reschedule
Connection count > 85% of CKU connection limit Warn New clients will be refused Scale CKUs; pool connections
Retained bytes trend Month-over-month above forecast Info Cost drift on the cold tier Review retention; budget GB growth

A sustained lag spike or a link leaving ACTIVE should page on-call and auto-open a ServiceNow incident, so a replication problem is a ticket with an owner, not a graph nobody watches.

Architecture at a glance

The shape is two Dedicated clusters and one managed link, wrapped in an operating model. Read the diagram left to right. The source in us-east-1 owns the writeable topics — producers only write here, scoped to WRITE on the settlement* prefix. A Cluster Link payments-east-to-west runs on the destination in eu-west-1 and continuously pulls the configured topics into read-only mirror topics that preserve partition count, keys, and exact offsets, while consumer.offset.sync.enable=true keeps committed group offsets in step so failover resumes cleanly. Each cluster independently runs Tiered Storage: the recent hotset stays on broker disk for low-latency tail reads while everything older offloads to object storage, so a topic holds seven years while brokers hold six hours. EU consumers read the mirror; if us-east-1 is lost, you failover the mirror to writeable and repoint producers — and because offsets matched, consumer groups resume where they were.

Around the data plane sit the operating tools, each mapping to a layer above: Terraform declares the environment, clusters, topics, link, mirrors, and ACLs; Vault issues every scoped credential as a short-lived lease; Okta/Entra SSO gates operators with MFA and RBAC bound to IdP groups; Jenkins/GitHub Actions plus Argo CD apply and reconcile the declared state; Datadog/Dynatrace ingest the Metrics API and alert on cluster_link_mirror_topic_offset_lag; ServiceNow records the failover change; Wiz audits the account and the Tiered Storage buckets. Follow producer → source topic → link → mirror → EU consumer, cold tier below each cluster, and the system explains itself.

Multi-region Kafka on Confluent Cloud: a us-east-1 Dedicated source cluster with writeable settlement topics, a destination-initiated Cluster Link pulling them into read-only mirror topics on a eu-west-1 Dedicated destination cluster with exact-offset fidelity and synced consumer offsets, each cluster offloading old log segments from its local-disk hotset to managed object storage via Tiered Storage, EU consumers reading the mirror, a failover path promoting the mirror to writeable on a regional outage, all wrapped by Terraform, Vault, Okta/Entra SSO, Jenkins/GitHub Actions, Argo CD, Datadog/Dynatrace, ServiceNow and Wiz

Real-world scenario

Veltrix Pay runs the settlement backbone for a card-issuing fintech: order, authorization, and settlement.completed events on a single Dedicated 4-CKU cluster in AWS us-east-1 — 18 topics, ~22,000 partitions, sustained ingress ~140 MB/s peaking to 380 MB/s at end-of-day settlement. Retention was 7 days on broker disk; nothing was replicated. Six platform engineers; monthly Confluent spend ~₹14,50,000 (~USD 17.5k). Two pressures landed the same quarter: a new EU banking entity needed local settlement that could survive a us-east-1 incident, and a regulator mandated seven-year retrievability of every settlement event as a replayable stream.

The first attempt was the obvious wrong one: MirrorMaker 2 on a small self-managed Connect cluster in eu-west-1 for the four settlement-critical topics. It demoed fine and fell apart in week two — the Connect cluster needed its own capacity planning, the offset-sync connector lagged under end-of-day load, and in a deliberate DR test the failed-over consumer groups restarted from the beginning of a 9-day topic, re-processing ~600 million events and double-firing settlement reconciliation. The post-mortem line: “we replicated the data but not the position, and added a system that fails on its own.”

They rebuilt on Cluster Linking and Tiered Storage. A second Dedicated 4-CKU cluster went up in eu-west-1 via Terraform with PrivateLink between the regions. One destination-initiated link, pay-east-to-west, replicated the four settlement topics — not all 18; clickstream and internal-ops topics stayed single-region to avoid cross-region egress Europe did not need. They set consumer.offset.sync.enable=true and consumer.offset.sync.ms=15000, and Schema Linking carried the Avro subjects so EU consumers could deserialize. On both clusters the four settlement topics got retention.ms = 7 years and confluent.tier.local.hotset.ms = 12 hours.

The numbers told the story. Moving the heavy settlement topics from 7-day broker retention to a 12-hour hotset shrank the hot-disk footprint enough to drop one CKU per cluster, and seven years of history accrued in object storage at roughly ₹2.20/GB-month. Cross-region mirroring billed on the ~140 MB/s of settlement bytes, not the full 380 MB/s — a deliberate egress line. The redone DR drill was the proof: they promoted the EU mirror in a maintenance window, repointed a canary producer, and the consumer groups resumed at their exact committed offsets — zero re-processing, zero double-settlement. Total spend landed at ₹16,80,000/month across two regions with seven-year retention — only ~16% more than the single-region, 7-day starting point.

The payoff came four months later when AWS us-east-1 had a multi-hour control-plane degradation. Veltrix failover-ed the four mirror topics, repointed producers to eu-west-1, and kept settling. Measured RPO was 1.8 seconds (the offset lag at the moment of failure, which they had alerted on and kept low); RTO was 11 minutes, almost all of it human decision-and-approval, not the technical cutover. The wall-mounted lesson: “Cluster Linking replicates the position, not just the bytes — that one difference is the entire value of doing DR right.”

The two designs, compared on what actually mattered:

Dimension MirrorMaker 2 (first attempt) Cluster Linking + Tiered Storage
Operator burden Self-managed Connect cluster to run/scale Fully managed link; nothing to operate
Offset fidelity on failover Broke — groups restarted from beginning Exact offsets preserved + synced
DR-drill result 600M events re-processed, double-settlement Zero re-processing, clean resume
Long retention Fragile lossy lake export 7y replayable in object storage
Measured RPO / RTO Untrustworthy 1.8 s / 11 min
Monthly cost (multi-region) Single region + a Connect cluster Two regions, 7y retention, ~₹16.8L

Advantages and disadvantages

The managed-link, tiered-storage model both enables multi-region long-retention Kafka and imposes real trade-offs. Weigh it honestly:

Advantages Disadvantages
Managed link — no Connect/MirrorMaker cluster to run, scale, or patch Cluster Linking is Dedicated-class; you pay for two Dedicated clusters
Exact offset preservation — failover resumes consumer groups correctly, natively Offset sync (consumer.offset.sync.enable) must be set at link creation; awkward to retrofit
Tiered Storage makes years of replayable retention economically real Cold-tier reads have higher latency; large backfills need planning
Read locality — EU consumers read a local mirror, not cross-region Mirror topics are read-only until promoted; apps must know not to write them
Zero-downtime migration via link → mirror → promote → repoint Bidirectional/active-active is more complex than the one-way DR shown here
Cross-region cost is opt-in per topic — replicate only what earns its keep Cross-region mirroring bills on bytes; careless replication of everything is expensive
Schema Linking keeps the destination able to deserialize Forgetting the schema strategy → EU deserialization failures
Independent tiering per cluster — each region controls its own retention Mirror topics tier independently; you must set destination retention or it defaults to 7 days

The model is right when you genuinely need a second region (DR, locality, or migration) and long retention on a Dedicated footprint you can justify. It is overkill for a single-region dev workload, and it is not a substitute for in-region HA — MULTI_ZONE handles a zone failure, the link handles a region failure. The disadvantages are all manageable, but only if you make the up-front decisions (Dedicated, offset sync, schema strategy, what-to-replicate) deliberately rather than discovering them in an incident.

Hands-on lab

Stand up the whole pattern at small scale, prove the three things that matter — data flows, offsets match, old data comes from the cold tier — execute a clean cutover, then tear it all down. This uses two Dedicated clusters (linking requires Dedicated), so it is not free-tier; budget a small spend and delete everything at the end (teardown is Step 11). Run in a shell with the confluent CLI logged in.

Step 1 — Pin the environment and set variables.

confluent login --save
confluent environment use env-payments-prod
SRC=lkc-source-use1     # replace with your real source cluster id after Step 2
DST=lkc-dest-euw1       # replace with your real destination cluster id after Step 2
LINK=payments-east-to-west

Step 2 — Provision two Dedicated clusters (or reuse existing). Smallest multi-zone is 2 CKUs; for a short lab a single-zone 1-CKU pair is cheaper if your environment allows it.

confluent kafka cluster create lab-source --cloud aws --region us-east-1 \
  --type dedicated --cku 1 --availability single-zone
confluent kafka cluster create lab-dest --cloud aws --region eu-west-1 \
  --type dedicated --cku 1 --availability single-zone
confluent kafka cluster list   # note the two lkc-... ids; set SRC and DST to them

Expected: two clusters provisioning; list shows PROVISIONING then UP after a few minutes. Wait for both to be UP before proceeding.

Step 3 — Create the source topic with Tiered Storage retention. Long retention, tiny hotset, so old reads hit the cold tier.

confluent kafka topic create settlement.completed --cluster $SRC \
  --partitions 6 \
  --config retention.ms=220752000000 \
  --config confluent.tier.local.hotset.ms=60000 \
  --config cleanup.policy=delete
confluent kafka topic describe settlement.completed --cluster $SRC

Expected: describe lists the configs back, including retention.ms=220752000000 and confluent.tier.local.hotset.ms=60000 — a 1-minute hotset, deliberately tiny so the lab can force a cold read fast.

Step 4 — Create a service account and a source-scoped key for the link.

confluent iam service-account create sa-lab-link --description "lab link principal"
confluent api-key create --service-account sa-lab-link --resource $SRC
# capture the printed key/secret into LINK_KEY / LINK_SECRET shell vars (do NOT commit)
confluent kafka acl create --cluster $SRC --allow --service-account sa-lab-link \
  --operations READ,DESCRIBE --topic settlement --prefix
confluent kafka acl create --cluster $SRC --allow --service-account sa-lab-link \
  --operations DESCRIBE --cluster-scope

Expected: an sa-... id, an API key/secret printed once, and two ACL rows created.

Step 5 — Create the Cluster Link on the destination, pointing at the source. Get the source bootstrap first.

SRC_BOOT=$(confluent kafka cluster describe $SRC -o json | jq -r '.endpoint')
cat > /tmp/link.properties <<EOF
link.mode=DESTINATION
connection.mode=OUTBOUND
bootstrap.servers=${SRC_BOOT}
security.protocol=SASL_SSL
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="${LINK_KEY}" password="${LINK_SECRET}";
consumer.offset.sync.enable=true
auto.create.mirror.topics.enable=false
EOF

confluent kafka link create $LINK --cluster $DST \
  --source-cluster $SRC --source-bootstrap-server ${SRC_BOOT} \
  --config-file /tmp/link.properties
confluent kafka link list --cluster $DST

Expected: the link appears and reaches ACTIVE within a minute or two. If it stays in a non-active state, jump to the troubleshooting table (most often a bad bootstrap or key).

Step 6 — Create the mirror topic on the destination.

confluent kafka mirror create settlement.completed --cluster $DST --link $LINK
confluent kafka mirror list --cluster $DST --link $LINK
confluent kafka topic update settlement.completed --cluster $DST \
  --config retention.ms=220752000000 \
  --config confluent.tier.local.hotset.ms=60000

Expected: mirror list shows settlement.completed in ACTIVE (or MIRROR) state with 6 partitions matching the source.

Step 7 — Produce on the source, consume the mirror on the destination. Prove data flows across the link.

# Produce 5 records on the SOURCE
for i in 1 2 3 4 5; do
  echo "{\"id\":\"s-100$i\",\"amt\":$((RANDOM%500))}"
done | confluent kafka topic produce settlement.completed --cluster $SRC

# Consume the MIRROR on the DESTINATION (should appear within seconds)
confluent kafka topic consume settlement.completed --cluster $DST \
  --from-beginning --print-key --timeout 10000

Expected: the five records appear on the destination within a few seconds — written in us-east-1, read in eu-west-1, over the managed link.

Step 8 — Prove offset fidelity. The mirror’s high-water marks should track the source partition-for-partition:

confluent kafka mirror describe settlement.completed --cluster $DST --link $LINK
# Look at per-partition lag (should be ~0) and that mirror offsets match the source

Expected: mirror describe shows per-partition lag at or near zero and offsets tracking the source — the offset-fidelity guarantee, visible.

Step 9 — Force a cold-tier read. With a 1-minute hotset, records older than a minute are served from object storage. Wait, then read from the beginning.

sleep 90                          # let the first records age past the 60s hotset
confluent kafka topic consume settlement.completed --cluster $SRC \
  --from-beginning --timeout 10000
# The earliest records now come from the COLD tier (object storage), transparently.

Expected: all records still return, including the earliest — but those are now fetched from the cold tier (in production, confirm via the Metrics API showing non-zero tiered-bytes-fetched).

Step 10 — Execute a graceful cutover (promote). Simulate a planned migration — the source is reachable, so use promote for a lossless cutover that makes the mirror writeable.

confluent kafka mirror promote settlement.completed --cluster $DST --link $LINK
confluent kafka mirror list --cluster $DST --link $LINK     # state -> PROMOTED
# The destination topic is now writeable — producers can target eu-west-1:
echo '{"id":"s-2001","amt":99}' | \
  confluent kafka topic produce settlement.completed --cluster $DST

Expected: the mirror moves to PROMOTED, and a produce against the destination now succeeds (it was rejected while the topic was a read-only mirror). That transition — read-only mirror to writeable topic — is the failover/migration mechanism.

Step 11 — Teardown (do this; Dedicated clusters bill continuously). Tear down in reverse dependency order.

confluent kafka mirror delete settlement.completed --cluster $DST --link $LINK
confluent kafka link delete $LINK --cluster $DST
confluent kafka cluster delete $DST
confluent kafka cluster delete $SRC
confluent api-key list --service-account sa-lab-link   # then delete the key
confluent iam service-account delete sa-lab-link

Expected: each resource deletes cleanly. Verify both clusters are gone with confluent kafka cluster list — a forgotten Dedicated cluster is the most expensive lab mistake you can make.

The lab steps mapped to what each proves, and its production analogue:

Step What you did What it proves Production analogue
2 Provisioned two Dedicated clusters Linking requires Dedicated The two-region footprint
3 Long retention + tiny hotset Retention decouples from disk 7-year retention, lean brokers
5 Destination-initiated link The link pulls; nothing to operate The managed replication pipe
6 Created a mirror topic Read-only, offset-faithful copy EU read locality + warm DR copy
7 Produced source, consumed mirror Data crosses the link Cross-region replication working
8 Compared offsets Exact offset fidelity Why failover resumes correctly
9 Forced a cold read Old data comes from object storage Audit/replay from the cold tier
10 Promoted the mirror Mirror → writeable is the cutover Zero-downtime migration / DR
11 Tore everything down Clean reverse-order teardown Avoiding orphaned Dedicated cost

Common mistakes & troubleshooting

The failure modes that bite, as a symptom → cause → confirm → fix playbook. Read the table first; expanded notes follow for the worst offenders.

# Symptom Root cause Confirm (exact command / path) Fix
1 link create rejected on the cluster Cluster is Basic/Standard, not Dedicated; or link already exists confluent kafka cluster describe <id>type Provision Dedicated; or delete the stale link first
2 Link stuck not ACTIVE after create Bad source bootstrap, wrong/expired link key, or network/PrivateLink not reachable confluent kafka link describe <link>; check bootstrap.servers + key Fix bootstrap/key in config; verify peering/PrivateLink to source
3 Failed-over consumer group restarts from beginning consumer.offset.sync.enable was not set at link creation confluent kafka link describe <link> → config has no offset sync Re-create the link with offset sync; cannot retrofit cleanly
4 EU consumers get deserialization errors Destination Schema Registry lacks the schemas Consumer logs: “Schema not found id=…”; check destination SR subjects Set up Schema Linking or a shared Stream Governance package
5 CKU cost exploded after enabling long retention Huge retention.ms with default (large) hotset → years on broker disk confluent kafka topic describeconfluent.tier.local.hotset.ms high/unset Set a small hotset.ms; it caps disk regardless of retention
6 App gets an error producing to the EU topic Writing to a read-only mirror topic before promotion confluent kafka mirror describe → state ACTIVE (still a mirror) Producers stay on the source until an explicit promote/failover
7 Topics you never wanted are replicating (and billing) auto.create.mirror.topics.enable=true pulled everything confluent kafka mirror list --link <link> shows unexpected topics Set it false; delete unwanted mirrors; create mirrors explicitly
8 Mirror lag growing steadily Link throughput < source ingress, or link unhealthy Metrics API cluster_link_mirror_topic_offset_lag climbing Add CKUs / check link health; replicate fewer topics
9 Destination keeps only ~7 days despite source 7y Mirror topic tiers independently; destination retention left default confluent kafka topic describe <topic> --cluster <DST> Set retention.ms/hotset.ms on the destination topic too
10 Partition counts don’t match across regions Source partitions changed after mirror creation confluent kafka topic describe both sides Mirrors inherit at create; recreate mirror after a source repartition
11 Cold-tier reads slow / cluster pressured during backfill Massive --from-beginning over a multi-TB topic in business hours Metrics show high tiered-fetch + elevated latency Schedule backfills off-peak; bound the read; size CKUs for it
12 promote/failover errors or hangs Wrong operation for source state (promote needs reachable source) confluent kafka cluster describe <SRC> reachability Use failover when the source is gone; promote when reachable
13 ACLs missing on the destination after failover acl.sync.enable was off confluent kafka link describe config; acl list on destination Enable acl.sync.enable; or re-create destination ACLs
14 Terraform wants to destroy/recreate the link every plan Provider drift on a config the API normalizes terraform plan diff on the link config block Pin provider version; align config to API-returned values; ignore_changes if needed

The expanded reasoning for the entries that cost the most time:

3. Failed-over consumer group restarts from the beginning. The expensive DR mistake — a failover that re-processes the whole topic and double-fires downstream side effects. The cause is almost always that consumer.offset.sync.enable was not enabled at link creation; without it the destination never knew where groups were, so a group resets to its auto.offset.reset (often earliest). Confirm with confluent kafka link describe <link>. You cannot cleanly retrofit it — re-create the link with offset sync on. This is why the decision belongs at design time, not after an incident.

8. Mirror lag growing steadily. On a forced failover, the source-to-mirror lag is your data loss. Steady growth means the link can’t keep up with source ingress (under-sized CKUs or too many replicated topics) or is degrading. Confirm via cluster_link_mirror_topic_offset_lag trending up; fix by adding CKUs, replicating only the topics that need a second region, and checking link health — never let lag silently grow. (Items 4 and 5 are covered in the Schema Linking and Tiered Storage sections above; the table rows are the quick reference.)

Best practices

Security notes

Every credential and path in this build is least-privilege and short-lived, because a payments backbone is a high-value target and a replicated one doubles the surface.

The security controls, what each defends, and what else it buys:

Control Mechanism Defends against Also prevents
Vault-leased scoped keys Per-principal API keys in Vault Static secrets in CI / git Broad blast radius on a leak
Least-privilege ACLs WRITE/READ/DESCRIBE per resource Over-privileged producers/consumers Accidental writes to a mirror
SSO + MFA + RBAC Okta/Entra → role bindings Shared logins, stale access Off-boarding gaps
TLS + BYOK + bucket audit SASL_SSL, customer keys, Wiz Cleartext, key drift, public buckets Compliance findings on the cold tier
Private networking PrivateLink / peering Public-internet replication path Link reachability surprises
Distinct link principal Separate service account Ambiguous audit trail “Who copied this?” uncertainty

Cost & sizing

Three levers dominate the bill, and they pull in different directions, so the goal is to right-size each rather than minimize any one.

CKU sizing drives each Dedicated cluster’s base cost, and two regions means two footprints. Size to sustained throughput with headroom for failover surge and cold-read backfills, not peak, and scale CKUs through Terraform when utilization climbs — over-provisioning “to be safe” is the most common waste. Tiered Storage is the cost win that makes seven-year retention viable: object storage is far cheaper per GB than broker disk, so a small confluent.tier.local.hotset.ms keeps the hot tier tiny while history accumulates cheaply — and shrinking the hot footprint can drop a CKU. Cluster Linking bills on bytes mirrored across regions, so replicate only what genuinely needs a second region; cross-region egress is a deliberate line item, not a side effect of auto.create.mirror.topics.enable=true.

The cost drivers, what each buys, the watch-out:

Cost driver What you pay for Rough magnitude What it buys Watch-out
CKUs (per cluster) Provisioned throughput/partition/connection capacity Largest line; per-CKU hourly ×2 regions Predictable, isolated performance Over-sizing two clusters; size to sustained load
Tiered Storage (object) GB-month in S3/GCS/Blob Low per GB (cents) Cheap years of replayable history Watch total GB grow over 7y; budget for it
Hot storage (broker disk) Disk for the hotset Tied to CKU/disk Low-latency tail reads Large hotset = expensive; keep it small
Cross-region mirroring Bytes mirrored over the link Per-GB egress on replicated topics Geo-replication / DR / locality Replicating everything; mirror only what’s needed
Schema Registry / Stream Governance Package (ESSENTIALS/ADVANCED) Modest fixed Schemas + Schema Linking ADVANCED needed for Schema Linking
Private networking PrivateLink/peering hourly + data Modest Secure, reliable inter-region path Needed for prod; small relative to CKUs

Right-size in order of leverage: shrink the hotset to hours (biggest saving, may drop a CKU); replicate only DR-critical topics (cuts cross-region bytes and destination storage); size CKUs to sustained and scale on metrics; and budget the cold tier’s GB growth explicitly. Pipe the Confluent billing and Metrics API into your cost dashboard so mirrored-GB and tiered-GB trends are visible. There is no free tier for Dedicated — the floor is two small Dedicated clusters, which is why the lab insists on teardown.

Interview & exam questions

1. Why is a Cluster Linking mirror topic better for DR than MirrorMaker 2? A mirror topic preserves the source’s exact offsets, partition count, and keys, and (with consumer.offset.sync.enable) syncs committed group offsets — so on failover a group resumes at the right position. MirrorMaker 2 republishes records with new offsets and needs separate offset-translation, which is fragile and the common cause of groups restarting from the beginning. Cluster Linking is also fully managed.

2. Cluster Linking is “destination-initiated.” What does that mean operationally? You create the link object and the mirror topics on the destination cluster, and the link pulls topics from the source. There is nothing to deploy or scale on the source side, and link create/mirror create always run against the destination. The destination dials out to the source’s bootstrap (connection.mode=OUTBOUND).

3. What is the single most important link setting for a clean failover, and why can’t you add it later? consumer.offset.sync.enable=true. It continuously copies committed consumer-group offsets from source to destination, so a failed-over group knows where it was. It must be set at link creation because it governs how offsets are tracked from the start; retrofitting it does not retroactively reconstruct the sync, so groups may still reset. Decide it at design time.

4. Difference between promote and failover on a mirror topic? Both convert a read-only mirror into a writeable topic. Promote is graceful — it first catches the destination fully up to a reachable source, so it is lossless; use it for planned migrations and drills. Failover is forced — it severs the link immediately when the source is unreachable, accepting the current replication lag as your RPO; use it in a real outage.

5. How does Tiered Storage make seven-year retention economically viable? It offloads closed segments older than the hotset to cheap object storage while keeping only the hotset on expensive broker disk. Set retention.ms to seven years and confluent.tier.local.hotset.ms to a few hours: brokers hold hours, object storage holds years, old offsets fetch transparently from the cold tier, and the topic stays a continuous, replayable Kafka topic.

6. What’s the relationship between retention.ms and confluent.tier.local.hotset.ms? retention.ms is how long data is kept anywhere before deletion; hotset.ms is how much recent data stays on local broker disk. The hotset caps hot-disk cost regardless of total retention — that decoupling is the whole point, and long retention with a small hotset is the cheap-and-long sweet spot.

7. Which cluster types support Cluster Linking, and which type does a production multi-region payments design need? Standard supports it with limits; Dedicated supports it fully (any direction) with isolated CKU capacity, full Tiered Storage control, private networking, BYOK, and the highest SLA. A mission-critical multi-region payments backbone needs Dedicated on both ends. Basic does not support production linking.

8. A consumer in the destination region throws “Schema not found.” What happened and how do you fix it? The mirror replicated the records, but each record’s embedded schema ID must resolve against the destination’s Schema Registry, which doesn’t have the schema. Fix by setting up Schema Linking to replicate subjects to the destination SR, or by sharing one Stream Governance package across both clusters, keeping compatibility modes aligned.

9. You enabled seven-year retention and your CKU cost jumped. What’s the most likely cause? You left the hotset large (or default) while setting a huge retention.ms, so years of data sit on expensive broker disk. Set confluent.tier.local.hotset.ms small (hours); it caps disk-resident data regardless of retention, pushing the rest into cheap object storage — often enough to drop a CKU.

10. Why must you set retention on the destination mirror topic, not just the source? Mirror topics tier independently of the source. If you only configure the source, the destination falls back to defaults (≈7 days) and you silently lose your long retention in the second region. Always set retention.ms and hotset.ms on both sides.

11. How do you keep cross-region replication cost under control? Keep auto.create.mirror.topics.enable=false and create mirror topics explicitly for only the topics that genuinely need a second region, so cross-region mirroring bytes are a deliberate choice. Monitor mirrored-GB via the Metrics API and challenge any topic whose replication isn’t earning its keep.

12. What does mirror lag tell you in DR terms, and how do you treat it? On a forced failover, the lag between source and mirror is exactly the data you may lose (your RPO). Treat it as a first-class SLO: alert on cluster_link_mirror_topic_offset_lag so a sustained spike pages on-call, and keep it low by sizing CKUs and replicating only what’s needed.

These map to the Confluent Certified Administrator for Apache Kafka (CCAAK) and Confluent Certified Developer for Apache Kafka (CCDAK) for the Kafka, replication, and retention concepts, and to general cloud-architecture DR objectives (RTO/RPO, multi-region) that appear across AWS / Azure / GCP professional architect exams. A compact mapping for revision:

Question theme Primary cert / domain Objective area
Mirror topics, offset fidelity, linking CCAAK Multi-cluster & replication
Tiered Storage, retention, hotset CCAAK Storage & cluster configuration
Schema Registry, Schema Linking, compatibility CCDAK Data formats & schema management
Promote/failover, RTO/RPO Cloud Pro Architect (any) Disaster recovery & resilience
Cluster types, CKU sizing, cost CCAAK / Cloud Pro Capacity planning & cost
ACLs/RBAC, scoped credentials CCAAK / Security AuthZ & secrets management

Quick check

  1. You create a Cluster Link but forget consumer.offset.sync.enable=true. A month later you do a real failover. What goes wrong, and why can’t you just fix it now?
  2. Your source topic has retention.ms set to seven years but your CKU bill jumped after you applied it. What single config did you most likely get wrong?
  3. True or false: a mirror topic is writeable as soon as it’s created, so EU producers can write to it directly for active-active.
  4. EU consumers throw “Schema not found” even though the mirror is healthy and records are arriving. What’s missing?
  5. When do you use promote versus failover on a mirror topic, and what’s the data-loss difference?

Answers

  1. The failed-over consumer groups restart from the beginning (or latest), re-processing or skipping data, because the destination never had their offsets synced. You can’t cleanly retrofit offset sync, so you must re-create the link with consumer.offset.sync.enable=true — which is why the decision belongs at design time.
  2. You left confluent.tier.local.hotset.ms large or default while setting a huge retention.ms, so years of data sit on expensive broker disk instead of cheap object storage. Set the hotset small (hours); it caps hot-disk cost regardless of total retention.
  3. False. Mirror topics are read-only until you explicitly promote or failover them. Producers stay on the source until a deliberate cutover; writing to a mirror returns an error. (Active-active is a different, bidirectional topology.)
  4. The destination Schema Registry doesn’t have the schemas the records’ embedded schema IDs reference. Set up Schema Linking (or share a Stream Governance package) so subjects replicate to the destination, with aligned compatibility modes.
  5. Use promote for a planned cutover when the source is reachable — it catches the destination fully up first, so it’s lossless. Use failover when the source is gone — it severs the link immediately and accepts the current replication lag as your RPO (potential data loss).

Glossary

Next steps

You can now stand up multi-region Kafka with offset-faithful replication and cheap long retention, and fail over cleanly. Build outward:

KafkaConfluent CloudCluster LinkingTiered StorageMulti-RegionDisaster RecoveryTerraformSchema Registry
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading