Message Ordering¶
Message ordering guarantees relative sequence for messages that share the same ordering key. This is essential for state-transition workflows where event chronology changes the final outcome.
In FastPubSub, ordered delivery requires coordinated publisher and subscriber configuration.
Conceptual Model¶
Ordering is scoped by key, not by topic.
- Messages with the same key are processed in publish order.
- Messages with different keys may be processed concurrently.
sequenceDiagram
participant P as Publisher
participant T as Topic
participant S as Subscriber
P->>T: Event A (key=user-42)
P->>T: Event B (key=user-42)
P->>T: Event C (key=user-77)
Note over S: A then B must keep sequence
Note over S: C may run in parallel path
Subscriber Configuration¶
@broker.subscriber(
alias="user-events-ordered",
topic_name="user-events",
subscription_name="user-events-ordered-subscription",
enable_message_ordering=True,
autocreate=True,
)
async def process_user_events(message: Message):
user_id = message.attributes.get("user_id", "unknown-user")
await update_user_state(user_id, message.data)
enable_message_ordering=True enables ordered delivery behavior for the subscription.
Publisher Configuration¶
# Create an ordered publisher
ordered_publisher = broker.publisher("user-events")
@app.post("/user-action")
async def user_action():
# Publish messages with ordering keys
await ordered_publisher.publish(
data={"action": "login", "user_id": "user-123"},
ordering_key="user-123",
attributes={"user_id": "user-123"},
)
# Guaranteed to be processed after the login message
await ordered_publisher.publish(
data={"action": "update_profile", "user_id": "user-123"},
ordering_key="user-123",
attributes={"user_id": "user-123"},
)
The publisher must send ordering_key for related events.
For handler logic and observability, include the same business identifier in attributes (for example user_id).
Both Ends Matter
Ordering requires publisher intent (ordering_key) and subscriber ordering configuration.
Missing either side weakens sequence guarantees.
Choosing Ordering Keys¶
Good ordering keys align with entity-level state boundaries.
| Domain | Recommended Key Pattern | Why |
|---|---|---|
| User lifecycle | user-{id} |
Preserves per-user chronology |
| Order state machine | order-{id} |
Keeps transitions deterministic |
| Inventory updates | sku-{id} |
Preserves stock mutation order |
| Account ledger | account-{id} |
Prevents out-of-order balance effects |
Anti-Patterns¶
- Single global key such as
all-events(destroys parallelism). - One key per message (provides no ordering value).
- Ephemeral keys unrelated to state boundaries.
Failure Behavior and Queue Blocking¶
With ordered delivery, a failed message blocks later messages with the same key.
@broker.subscriber(
alias="ordered-processor",
topic_name="ordered-events",
subscription_name="events-ordered-subscription",
enable_message_ordering=True,
dead_letter_topic="events-dlq",
max_delivery_attempts=5,
autocreate=True,
)
async def process_ordered(message: Message):
await process_event(message.data)
For this reason, ordered subscriptions should be paired with dead-letter policy. Without bounded retries, one poison message can stall an entire entity stream.
Throughput Implications¶
Ordering introduces serial execution within each key lane.
graph LR
subgraph Unordered
A1[Msg A] --> P1[Process]
B1[Msg B] --> P2[Process]
C1[Msg C] --> P3[Process]
end
subgraph Ordered same key
A2[Msg A] --> P4[Process] --> B2[Msg B] --> P5[Process] --> C2[Msg C]
end
Plan capacity around key cardinality:
- More keys usually increase effective parallelism.
- Very hot keys create natural bottlenecks.
Representative Use Cases¶
Session Event Tracking¶
@broker.subscriber(
alias="session-tracker",
topic_name="session-events",
subscription_name="session-events-subscription",
enable_message_ordering=True,
)
async def track_session(message: Message):
user_id = message.attributes.get("user_id", "unknown-user")
event = message.data
# Events arrive in order: login → page_view → purchase → logout
await session_store.append_event(user_id, event)
State Machine Transitions¶
@broker.subscriber(
alias="order-state",
topic_name="order-events",
subscription_name="order-state-subscription",
enable_message_ordering=True,
)
async def process_order_state(message: Message):
import json
order_id = message.attributes.get("order_id", "unknown-order")
transition = json.loads(message.data)["transition"]
# Transitions arrive in order: created → paid → shipped → delivered
await state_machine.transition(order_id, transition)
Inventory Mutation Stream¶
@broker.subscriber(
alias="inventory-updater",
topic_name="inventory-events",
subscription_name="inventory-subscription",
enable_message_ordering=True,
)
async def update_inventory(message: Message):
import json
sku = message.attributes.get("sku", "unknown-sku")
delta = json.loads(message.data)["quantity_change"]
# +10, -5, +3 applied in correct order
await inventory_db.update_quantity(sku, delta)
Design Recommendations¶
Carry Business Keys in Attributes¶
Even when ordering_key is used for transport sequencing, include a readable identifier in attributes for logs,
test assertions, and incident diagnosis.
Keep Handler Latency Predictable¶
Long-running handlers delay all subsequent messages for the same key. Move heavy side work to asynchronous downstream flows when possible.
Observe Per-Key Backlog¶
Aggregate queue metrics can look healthy while a single key lane is blocked. Monitor key-level hotspots where operational tooling allows.
Common Failure Modes¶
- Enabling ordering without dead-letter policy and bounded retries.
- Using a global ordering key and unintentionally serializing all traffic.
- Assuming ordering is global across keys instead of key-scoped.
- Ignoring hot-key skew during capacity planning.
Recap¶
- Ordering guarantees sequence only for messages that share a key.
- Configure subscriber ordering and publish with
ordering_key. - Design keys around entity state boundaries.
- Pair ordering with dead-letter policy to avoid indefinite blocking.
- Treat ordering as a correctness tool with explicit throughput trade-offs.