Kafka Cross-Region Replication Patterns: Hub-and-Spoke, Mesh, and When to Use Neither
Kafka cross-region replication comes down to a small set of named topologies: active-passive, active-active, hub-and-spoke, and mesh. Most teams need one of the first two, and both already have a full implementation guide on this site. This post covers the other two — the ones that show up as a single sentence in every comparison article and never get built out: hub-and-spoke fan-in with real MirrorMaker 2 configuration, and an honest account of what mesh actually costs before you reach for it.
Hub-and-spoke is active-active's asymmetric cousin — spokes replicate up to a hub, the hub does not have to replicate back down — and it is straightforward to build with MirrorMaker 2's per-pair flow configuration. Mesh is not: its flow count grows as N×(N-1), and almost nothing needs it.
Where this fits among the other replication patterns
If you have not yet chosen a pattern, start with Kafka geo replication, which compares all four side by side on RTO, cost, and complexity. Two of the four already have their own deep dive:
- Active-passive — one cluster serves traffic, a standby receives a continuous copy and waits. The promotion runbook, consumer offset translation on failover, and failback are covered in Kafka active-passive DR architecture.
- Active-active — two or more clusters all serve traffic, with bidirectional replication keeping them converged. Loop prevention, offset handling between live clusters, and the symmetric aggregation-subscription pattern are covered in Kafka active-active replication.
That leaves hub-and-spoke and mesh, which is where this post spends its time.
Hub-and-spoke is not active-active's aggregation pattern
It is easy to conflate the two, because both involve one cluster seeing data that originated in several others. The difference is symmetry.
In active-active, every cluster is a peer. Each one produces locally and replicates outward, and any cluster can subscribe to the combined stream by matching its own local topic plus every other cluster's prefixed copy. That is a symmetric mesh of exactly the size the topology needs, described in full in the active-active post above.
Hub-and-spoke is asymmetric. Spoke clusters replicate up to a hub; the hub is not required to replicate anything back down. A spoke does not see other spokes' data unless you deliberately wire that up. This fits topologies where several independent sites — regional deployments, edge locations, business units — need to feed one central place for analytics, archival, or a global read model, without those sites needing to see each other.
Implementing hub-and-spoke with MirrorMaker 2
MirrorMaker 2's {source}->{target} flow-key syntax is not limited to a
single pair. One mm2.properties file can name any number of cluster
aliases and enable independent flows between any subset of them, each
with its own topics, groups, and tasks.max — a fact already verified
against Apache Kafka's own geo-replication documentation in Kafka
MirrorMaker 2 architecture. Fan-in
is one specific way to use that flexibility: enable a flow from every spoke
into the hub, and enable none in the other direction.
clusters = hub, edge-eu, edge-us, edge-apac
hub.bootstrap.servers = kafka-hub.example.com:9092
edge-eu.bootstrap.servers = kafka-eu.example.com:9092
edge-us.bootstrap.servers = kafka-us.example.com:9092
edge-apac.bootstrap.servers = kafka-apac.example.com:9092
# Each spoke replicates up to the hub. No hub->spoke flows exist.
edge-eu->hub.enabled = true
edge-eu->hub.topics = orders, inventory
edge-us->hub.enabled = true
edge-us->hub.topics = orders, inventory
edge-apac->hub.enabled = true
edge-apac->hub.topics = orders
# Offset sync per spoke, so a hub-side consumer group can be
# reasoned about relative to each source's committed offsets.
edge-eu->hub.emit.checkpoints.enabled = true
edge-us->hub.emit.checkpoints.enabled = true
edge-apac->hub.emit.checkpoints.enabled = true
DefaultReplicationPolicy is what keeps this from colliding. orders from
edge-eu lands on the hub as edge-eu.orders; orders from edge-us
lands as edge-us.orders. Three independently operated spokes writing to a
topic of the same name never overwrite each other at the hub, because the
alias prefix — not the topic name — is what identifies the source.
Notice what is absent: no hub->edge-eu.enabled, no reverse flow of any
kind. That asymmetry is the entire difference from active-active. Each
spoke flow is also independently scoped — edge-apac replicates only
orders, not inventory — because independence is exactly what the
flow-key syntax was designed to give you.
Consuming at the hub
A hub-side consumer that wants the full picture across regions subscribes
to the union of prefixed topics — a regex like .*\.orders picks up
edge-eu.orders, edge-us.orders, and any future spoke's prefixed copy
without a config change. A consumer that only cares about one region
subscribes to that region's prefixed topic directly.
Spoke-local consumers are unaffected by any of this. An application reading
orders inside edge-eu never sees edge-us.orders or edge-apac.orders
— those only exist on the hub cluster. This is the opposite of
active-active's aggregation subscription, where every peer cluster carries
every other peer's prefixed copy by design.
Operating a hub-and-spoke topology
Three operational properties matter once the topology is running:
A lagging spoke should not degrade the hub or its siblings. Each
<spoke>->hub flow is an independent set of Connect tasks with its own
lag. Monitor per-flow lag, not just aggregate hub-side consumer lag — a
single slow spoke will not visibly show up in a hub consumer group's
overall lag if the other spokes are healthy, and it can hide until that
spoke's data is stale enough to matter.
One spoke's outage does not touch the others. Because there is no
spoke-to-spoke flow, edge-us going offline stops the edge-us->hub flow
and nothing else. edge-eu and edge-apac keep replicating normally. This
blast-radius isolation is a direct consequence of the asymmetric design —
a mesh topology does not get this property for free, because a cluster
in a mesh is both a source and a target for every other member.
Onboarding a new spoke means adding one flow, not renegotiating the
topology. A new edge-latam alias with its own edge-latam->hub.enabled = true flow joins the file without touching the existing three. Compare
this to mesh, where adding one member means adding a flow in both
directions to every existing member.
Mesh: the honest version
Mesh replicates every cluster to every other cluster, bidirectionally. It shows up in comparison tables as the "everyone sees everyone" option, and it is almost always the wrong answer. The reason is arithmetic, not opinion.
A full mesh needs N×(N-1) directional flows — every ordered pair, since a mesh is bidirectional between each pair. Three clusters need 6 flows. Five clusters need 20. Ten clusters need 90.
| Clusters (N) | Directional flows (N×(N-1)) |
|---|---|
| 3 | 6 |
| 5 | 20 |
| 10 | 90 |
Every one of those flows is an independent MM2 connector pair: its own
lag, its own failure mode, its own topic-naming exposure. And because
DefaultReplicationPolicy prefixes topics with their source alias on every
hop, a record that crosses more than one link in a multi-hop path can
accumulate more than one prefix by the time it reaches a distant cluster —
the same alias-prefix mechanic that gives active-active its loop
prevention becomes a naming problem at mesh scale, where "which cluster did
this originally come from, and how many hops did it take" stops being
obvious from the topic name alone.
None of this means mesh is impossible to run. It means the operational surface — monitoring, alerting, and reasoning about topic names — grows faster than the cluster count, while the actual requirement driving most mesh proposals ("everyone needs everyone else's data") is usually satisfied by a topology that grows linearly instead.
What to build instead of mesh
Before configuring N×(N-1) flows, separate the requirement from the instinct. Two questions narrow it fast:
Does every cluster genuinely need every other cluster's data, or does something need the combined view? If the actual need is one place that sees everything — analytics, a global read model, compliance archival — that is hub-and-spoke, not mesh. It is the pattern this post just walked through, and it grows by one flow per new member instead of N-1.
Does only a specific pair of regions need direct replication? If two regions genuinely serve as each other's failover target and nothing else needs that link, that is a deliberate point-to-point active-passive or active-active pair — covered in the posts linked above — not a reason to wire up every other cluster too.
In both cases, the fix is the same: name the actual requirement, then pick the smallest topology that satisfies it. Mesh earns its cost only when the requirement really is full bidirectional connectivity between every member, which is rare enough that it is worth stating out loud before building it.
Choosing among the four patterns
| Pattern | Topology shape | Flows required | Best for | Implementation |
|---|---|---|---|---|
| Active-passive | One-way, two clusters | 1 | DR for a single primary region | Active-passive DR |
| Active-active | Bidirectional, peers | 2 per pair | Regional serving with failover both ways | Active-active replication |
| Hub-and-spoke | Asymmetric fan-in | 1 per spoke | Central aggregation from independent sites | This post |
| Mesh | Bidirectional, all pairs | N×(N-1) | Genuine full connectivity — rare | This post |
Replication is not a backup, in any of these shapes
None of these patterns give you point-in-time recovery. A bad write, a deletion, or a corrupted record replicates through a hub-and-spoke flow or a mesh link exactly as faithfully as a good one — hub-and-spoke fans it out to one central place, mesh fans it out to everywhere, and neither one gives you a version of the topic from before it happened. The disaster recovery use cases cover the broader set of failure modes a backup plan needs to account for, on top of whichever replication topology you run.
Frequently asked questions
How is hub-and-spoke different from active-active's aggregation pattern?
Symmetry. Active-active clusters are peers — each one produces, replicates outward, and can subscribe to every other peer's data. Hub-and-spoke is asymmetric: spokes replicate up to a hub, and the hub does not replicate back down unless you deliberately configure that. A spoke never sees another spoke's data by default.
Does a lagging spoke slow down the hub or other spokes in a hub-and-spoke topology?
No, not directly. Each spoke-to-hub flow runs as its own set of MirrorMaker 2 Connect tasks with independent lag. A slow or offline spoke does not affect the other spokes' flows or the hub's ability to consume their data — but it can hide inside aggregate hub-side consumer lag metrics, so monitor per-flow lag rather than only the consumer group total.
How many MirrorMaker 2 flows does a mesh of N clusters actually need?
N×(N-1) directional flows, since a full mesh replicates every ordered pair bidirectionally. Three clusters need 6 flows, five need 20, and ten need 90 — each one an independent connector pair with its own lag and failure mode to monitor.
Is hub-and-spoke or mesh replication a substitute for backup?
No. Both patterns move data between clusters; neither provides a point-in-time copy you can restore from. A corrupted or deleted record replicates through a hub-and-spoke flow or a mesh link just as faithfully as valid data, so an independent backup remains necessary regardless of which cross-region topology you run.
Can I add a new spoke to a hub-and-spoke topology without touching the existing configuration?
Yes. Because each spoke's flow to the hub is independently configured with its own `<spoke>->hub.enabled` entry, adding a new spoke means adding one new flow block to the mm2.properties file. The existing spokes' flows are untouched — unlike mesh, where a new member requires a new bidirectional flow to every existing member.