Skip to content
About
Main content

Content-based and affinity routing

What does routing by content or by affinity cost, and why should routing fields live in headers?

Content-based routing

destination = predicate(record.content)

Route by type, region, tenant, priority, schema version, or any field in the record.

It earns its place for: routing to specialized handlers, regional data residency (EU data stays in the EU), tenant isolation, splitting a mixed stream by event type, versioned processing paths, and gradual rollout by attribute.

The costs, which are structural

  • The router must parse. Every record is deserialized to make the routing decision. At high volume that is real CPU. This is why headers (also called attributes or properties) exist: Kafka headers, SNS message attributes, AMQP properties. Put routing-relevant fields in headers, not in the body. Then the router reads small metadata and never touches the payload. It is a small design decision with a large effect.
  • The router becomes a coupling point. Its rules encode knowledge about both the 's semantics and the ' topology. Changing a rule is a deployment. Rule sets grow, and nobody dares to delete a rule.
  • Rules and topology drift apart. A predicate that matches nothing is invisible. A record that matches no predicate must go somewhere, and "somewhere" is a default route that you must define explicitly, or the record is silently dropped. This is the most common silent-data-loss mechanism in content routing.
  • Ordering fractures. Two records for the same entity that match different predicates go to different destinations and lose their relative order. If you route by mutable state (status = pending versus status = complete), you have sent one entity's lifecycle along different paths, and the order is gone exactly where it mattered most.
  • Overlapping predicates. First-match (stop at the first rule that fits) and all-match (deliver to every rule that fits) are different meanings with very different results. State which one you use.

The architectural alternative

Instead of a smart router, publish everything to one and let each consumer filter. This moves the cost from central routing to distributed reading, removes the coupling point and lets consumers change their interest without a router deployment. The price is read amplification: every consumer reads everything.

Broker-side filtering is the middle ground. The subscriber declares a predicate and the evaluates it (SNS filter policies, JMS selectors, Pulsar, Solace, MQTT topic patterns). There is no read amplification on the wire, no central rule set, and each subscriber owns its own interest. This is the right default for with selection (one record to many subscribers, each wanting only some).

The hierarchical topic trick. A topic name such as orders.us-east.retail.created, with wildcard subscriptions such as orders.*.retail.*, gives content routing with zero payload inspection: the routing key is the metadata. Push the routing dimensions into the topic name and the whole problem gets cheaper. The limit: a hierarchy is a fixed order of dimensions. You cannot subscribe efficiently across a dimension you placed too deep.


Affinity routing and sticky sessions

Affinity routing sends a logical stream of related requests to the same instance. It is the same idea as key partitioning, applied to request routing instead of record placement.

Why you would do it

  • Local state and a warm cache. The instance already has the session, the compiled query, the open connection or the loaded model. This is the real motivation today. It is no longer session state (everyone moved that out of the instance) but cache locality. If 90% of one key's requests go to one node, that node's cache is effective. With , every node's cache holds only 1/N of the working set.
  • A single writer per entity. Affinity gives you serialized access to an entity without locks. It is the same trick as key partitioning: .
  • Connection reuse for long-lived streams and WebSockets (a WebSocket is a long-lived two-way connection between a browser and a server).

The costs

  • Load balancing gets worse. Sessions have unequal weight. You are now balancing sessions, not requests, and the weight of a session is unknown when you assign it.
  • A node failure loses the state of its affinity group unless that state is replicated. Consistent or at least make the reassignment minimal and deterministic.
  • Scale-out does not help existing sessions. New capacity serves only new sessions. Under a sustained spike from existing sessions, adding nodes does nothing. This is a surprising failure mode.
  • Deploys are disruptive in a way stateless deploys are not.

The distinction worth making

Hard affinity means "must go here": correctness depends on it (for example, single writer). means "prefer here": only performance depends on it (for example, cache warmth). Soft affinity with a fallback to any node is far easier to operate. You keep most of the locality benefit and none of the availability coupling. If you can make your affinity soft, do. is essentially soft affinity done properly.