Skip to content

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.