Message Ordering Guarantees in Webhook Architecture
Understanding Webhook Message Ordering
Message ordering is the part of Webhook Architecture Fundamentals & Design Patterns that determines the deterministic sequence in which event consumers process payloads, and it is where naive integrations most often corrupt state. In webhook architectures, three primary ordering models exist: FIFO (strict per-resource sequence), causal (happens-before dependency tracking), and total order (global sequence across all tenants). Asynchronous HTTP delivery inherently breaks naive sequencing because TCP retransmissions, load balancer routing decisions, network jitter, and concurrent producer scaling introduce non-deterministic latency. Unlike synchronous RPC calls, webhook dispatch operates on a fire-and-forget model where delivery acknowledgments do not guarantee arrival sequence. This guide assumes you control the consumer ingress layer and can persist sequence state; the foundational delivery mechanics establish why consumers must implement explicit sequence reconciliation rather than relying on transport-layer guarantees.
Failure Mode Analysis
- Network Jitter & Reordering: Variable RTT across CDN edges or ISP hops causes payloads dispatched at
T1to arrive afterT2. - Concurrent Producer Scaling: Horizontal scaling of webhook dispatch workers generates overlapping timestamps without a shared sequence coordinator.
- Retry Storms: Exponential backoff on failed deliveries disrupts original dispatch order, injecting stale payloads into active processing windows.
Implementation Patterns & Security Controls
- Monotonic Sequence IDs: Attach an incrementing, resource-scoped integer to every payload. Consumers reject payloads with
seq_id < last_processed. - Vector Clocks for Causal Ordering: Maintain
[node_id, counter]tuples to reconstruct partial ordering when strict FIFO is impossible. - Partition-Keyed Routing: Hash tenant or resource IDs to deterministic dispatch queues, ensuring per-partition FIFO.
- Sequence-Aware HMAC Validation: Bind cryptographic signatures to
seq_idto prevent replay attacks using reordered payloads. - Anti-Replay Window Enforcement: Maintain a sliding window of accepted sequence IDs, rejecting duplicates outside the tolerance threshold.
Operational Workflows
- Sequence Gap Detection Alerts: Trigger PagerDuty/Slack notifications when
current_seq - last_seq > 1. - Consumer Lag Monitoring Dashboards: Track
dispatch_timestamp - process_timestampdelta per partition to identify sequencing bottlenecks.
Runnable Implementation: Sequence Validation Middleware
import hmac
import hashlib
from typing import Set
class SequenceValidator:
def __init__(self, secret: str, window_size: int = 100):
self.secret = secret.encode("utf-8")
self.window_size = window_size
self.processed_sequences: Set[int] = set()
def validate(
self, payload: bytes, signature: str, seq_id: int, timestamp: str
) -> bool:
# 1. Anti-replay window check
if seq_id in self.processed_sequences:
return False
# 2. Sequence monotonicity enforcement
if self.processed_sequences:
min_allowed = max(0, max(self.processed_sequences) - self.window_size)
if seq_id < min_allowed:
raise ValueError(
f"Sequence {seq_id} outside replay window (min: {min_allowed})"
)
# 3. Cryptographic verification
expected_sig = hmac.new(self.secret, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, signature):
raise ValueError("HMAC signature mismatch")
self.processed_sequences.add(seq_id)
return True
Troubleshooting Steps
- Symptom: Consumer processes
seq_id: 5beforeseq_id: 4. Action: Verify partition routing hash function. Ensure identical tenant IDs map to identical dispatch queues. - Symptom:
HMAC signature mismatchon valid payloads. Action: Confirm signature generation includes the exactseq_idand raw payload bytes. Strip whitespace/normalize JSON before signing. - Symptom: Sequence window fills rapidly, rejecting valid payloads.
Action: Increase
window_sizeor implement persistent sequence tracking (Redis sorted set) instead of in-memory sets.
Architectural Patterns for Guaranteed Sequencing
Guaranteed sequencing requires either broker-managed ordering or application-layer coordination. Broker-managed solutions (e.g., AWS SQS FIFO, Kafka partitions) enforce strict ordering at the transport layer but introduce vendor lock-in and partition rebalancing overhead. Application-layer sequencing decouples ordering from infrastructure by embedding deterministic metadata directly in payloads. When defining payload schemas, align with Event Schema Design to embed sequence counters, causality tokens, and version tags without inflating payload size or violating size constraints. The pragmatic middle ground — global ordering abandoned, ordering preserved only within each key — is worked through end to end in per-key ordering with partitioned queues, and it is what almost every production platform actually ships. Whichever route you take, the consumer runs a small state machine per partition: it holds a cursor at the next expected sequence, buffers anything that arrives early, and has exactly one escape hatch when the gap never fills.
Failure Mode Analysis
- Partition Rebalancing: Broker consumer group rebalancing temporarily breaks sequence continuity during node scaling or failure.
- Consumer Lag Staleness: Slow consumers apply outdated sequence states, causing downstream state corruption.
- Duplicate Sequence IDs: Producer crashes during sequence ID generation lead to ID reuse, breaking monotonic guarantees.
Implementation Patterns & Security Controls
- Strict Partition Routing by Tenant/Resource ID: Use consistent hashing (
hash(resource_id) % partition_count) to bind events to fixed queues. - In-Memory Sequence Window Buffers: Maintain a bounded priority queue that holds out-of-order payloads until gaps are filled.
- Gap-Filling Reconciliation Loops: Poll producers or query audit logs for missing
seq_idvalues and request re-delivery. - Cryptographic Sequence Chaining: Hash each payload’s signature into the next payload’s
prev_hashfield, creating an immutable chain. - Rate Limiting per Partition: Prevent starvation by capping dispatch rates per tenant, ensuring fair sequencing progression.
Operational Workflows
- Automated DLQ Routing for Out-of-Sequence Payloads: Route payloads exceeding reorder timeout thresholds to a dead-letter queue for manual reconciliation.
- Sequence Drift Alerting Thresholds: Monitor
max(seq_id) - min(seq_id)across partitions. Alert when drift exceeds configurable SLAs.
Runnable Implementation: Gap-Filling Reorder Buffer
import heapq
import time
from typing import List, Optional, Tuple
class ReorderBuffer:
def __init__(self, max_wait_ms: int = 5000, max_buffer_size: int = 50):
self.max_wait_ms = max_wait_ms
self.max_buffer_size = max_buffer_size
# (seq_id, enqueue_time_ms, payload)
self.buffer: List[Tuple[int, float, bytes]] = []
self.next_expected: int = 0
def enqueue(self, seq_id: int, payload: bytes) -> Optional[bytes]:
if seq_id == self.next_expected:
self.next_expected += 1
return payload
if len(self.buffer) >= self.max_buffer_size:
raise BufferError("Reorder buffer full. Dropping payload.")
heapq.heappush(self.buffer, (seq_id, time.time() * 1000, payload))
return self._drain_ready()
def _drain_ready(self) -> Optional[bytes]:
ready = []
while self.buffer:
seq_id, ts, payload = self.buffer[0]
if seq_id == self.next_expected:
heapq.heappop(self.buffer)
ready.append(payload)
self.next_expected += 1
elif (time.time() * 1000 - ts) > self.max_wait_ms:
heapq.heappop(self.buffer) # Expired — route to DLQ externally
else:
break
return ready[0] if ready else None
Troubleshooting Steps
- Symptom: Buffer consistently overflows during peak traffic.
Action: Increase
max_buffer_sizeor reducemax_wait_ms. Implement persistent storage (e.g., Redis) for high-throughput environments. - Symptom: Rebalancing causes 10–20 second sequence gaps. Action: Enable broker-side sticky partition assignment and implement warm-up pre-fetching before processing resumes.
- Symptom: Duplicate
seq_iddetected after producer restart. Action: Implement atomic sequence generation using database sequences or distributed counters (e.g., Snowflake IDs) instead of local counters.
Sizing the Reorder Buffer and Its Timeout
Every reorder buffer is defined by two numbers, and both are usually picked by guesswork and then never revisited. max_wait_ms is not a tuning knob for the buffer — it is a direct addition to the worst-case processing latency of an entire partition, because a held cursor blocks every subsequent event for that key. max_buffer_size is not a memory limit either; it is the point at which your ordering guarantee converts into either backpressure or data loss, depending on which branch you wrote.
Start by measuring the distribution of reorder distances in your own traffic, because it is almost always bimodal and that shape decides the timeout. Gaps caused by concurrent dispatch workers or in-flight network jitter close within a few tens of milliseconds — one round trip. Gaps caused by a failed delivery close only when the retry fires, which for most senders is 60 seconds or more. There is very little mass between those two modes. A max_wait_ms of 2,000 to 5,000 therefore captures essentially all of the recoverable reordering while giving up quickly on the gaps that were never going to close inside a reasonable window; raising it to 45 seconds in the hope of catching the retry mode simply adds 45 seconds of stall to every partition that hits a gap, and does so most often precisely when the system is already degraded.
Buffer capacity should be bounded by bytes as well as by count, and the arithmetic is unforgiving. A partition taking 500 events per second with a 5-second wait can hold 2,500 events at the moment a gap opens; at a 2 KB average payload that is 5 MB for one partition, and a worker owning 64 partitions can be asked for 320 MB in the worst case. On a container with a 512 MB limit, the OOM killer arrives before the timeout does, and the observable symptom is a worker that restarts under load with no application error — just a 137 exit code and a partition whose lag graph resets to zero after every crash. Bound the buffer by total resident bytes, spill to Redis rather than to the heap when a partition exceeds its share, and treat “buffer full” as backpressure to the queue rather than as an exception.
That high-water mark matters more than it first appears, because the most common permanent gap is not a lost event — it is a sequence number that was never issued at all. Subscription filtering does this routinely: the producer increments a per-resource counter for every state change, then only dispatches the event types this endpoint subscribed to, so the consumer sees 41, 42, 44 and waits for a 43 that exists only in the producer’s database. The symptom is a partition that stalls for exactly max_wait_ms on a recurring basis, always on the same event types, with the dead-letter queue filling with events that are perfectly valid. Fix it at the source by numbering the delivered stream per subscription rather than the internal change stream, or by emitting a periodic heartbeat carrying the current high-water mark so consumers can distinguish “not yet” from “never”.
Consumer startup is the second reliable stall. A cursor held only in memory resets on deploy, and a consumer that restarts at zero treats every incoming event as early, buffers the lot, and flushes the whole partition to the dead-letter queue one timeout later. Persist the cursor with the same commit that applies the side effect — a last_applied_seq column on the resource row is the cheapest correct implementation — and on startup seed the buffer from that value rather than from the first event observed. The distinctive symptom of getting this wrong is a dead-letter spike whose timestamps line up exactly with your deploy markers and which disappears if you replay the same traffic against a warm consumer.
Partition-count changes cause the third stall, and this one is permanent rather than transient. Routing with hash(resource_id) % partition_count reassigns most keys the moment the modulus changes: going from 8 partitions to 12 moves roughly two-thirds of all resources to a new partition, where their in-flight predecessors are sitting in a buffer that no longer receives their successors. Both partitions stall, then both dead-letter. Use a consistent-hash ring or a stored key-to-partition map so that adding capacity moves a bounded slice of keys, and drain the old assignment before serving the new one — never rehash in place while traffic is flowing.
What Ordering Costs in Throughput and Blast Radius
Ordering is not free, and the price is paid in concurrency. Strict per-partition ordering means exactly one event per partition may be in flight at a time, so the ceiling on throughput is the partition count divided by the per-event service time, no matter how many workers you run. With a 40 ms handler, one partition sustains 25 events per second; 64 partitions cap the system at 1,600 events per second even with 500 idle workers attached. Reaching 5,000 events per second from there requires either 200 partitions or a 12 ms handler — and it is worth being explicit that adding workers, the reflex fix for every other throughput problem, does nothing at all here.
Blast radius is the second cost and the one that generates the support tickets. Because a partition is shared by many keys, one slow or repeatedly failing resource stalls every other resource that hashes to the same place. With 50,000 tenants across 64 partitions, roughly 780 tenants share each partition, so a single customer whose endpoint takes 30 seconds per call holds up 779 unrelated customers. The lag graph makes this unmistakable: one partition’s consumer lag climbs linearly while the other 63 sit flat, and the tickets arrive from tenants who have no idea why their events are 20 minutes late. Two mitigations actually work — partition on the finest key the business requires (a resource id rather than a tenant id shrinks the shared set by orders of magnitude), and enforce a hard per-event handler timeout so that one pathological consumer costs a bounded amount of head-of-line time rather than an unbounded one.
Skew makes the same problem worse without changing anything else. Real tenant distributions are heavily skewed toward a few large accounts, and it is normal for the largest single account to generate 20–30% of all events; hashing it to a partition makes that partition permanently hotter than the rest, and no amount of extra partitions fixes it because a single key cannot be split without abandoning its ordering guarantee. Detect it by alerting on the ratio of maximum to median per-partition lag — anything sustained above 5 is skew, not load — and handle it by giving the outlier key its own dedicated partition or its own consumer group, which is the one case where manual partition assignment beats hashing.
The cheapest ordering strategy of all is to design so that ordering is not required. If every event carries a monotonic resource_version and the consumer applies an update only when the incoming version exceeds the stored one, arrival order stops mattering: the final state converges to the highest version regardless of the sequence in which events land, late duplicates become no-ops, and the reorder buffer, the partition ceiling and the head-of-line blocking all disappear together. This works whenever events carry full state rather than deltas, which is a schema decision made once in Event Schema Design and repaid forever. Deltas — balance_changed_by: -50 — are the case where order genuinely cannot be relaxed, because applying them in a different order produces a different answer. Before building a sequencing pipeline, check whether the payloads could carry state instead of deltas; a great many ordering requirements dissolve at that point, and the ones that survive are the ones worth the partition budget.
Sequencing also interacts with the delivery contract underneath it. Ordered delivery on top of at-least-once transport still hands the consumer duplicates, and a duplicate applied out of order is exactly as damaging as a reordered original, which is why the version check or idempotency key belongs inside the same transaction that advances the cursor. The choice of contract and what it costs is worked through in at-least-once vs exactly-once delivery trade-offs.
Security Controls and Operational Workflows
Reordered payloads interact dangerously with state mutation and deduplication logic. If a consumer applies a user.updated event before a user.created event, downstream databases may throw constraint violations or silently corrupt records. Ordering guarantees must be paired with safe retry mechanisms to prevent cascading failures. Integrate Idempotency in Webhooks when detailing how consumers should handle duplicate or out-of-order deliveries without corrupting downstream state. Concretely, that means the wire format has to carry the ordering metadata and bind it cryptographically — a sequence number an attacker can strip or reorder without invalidating the signature is decoration, not a control.
Failure Mode Analysis
- Signature Mismatch from Timestamp Drift: Clock skew across distributed consumers invalidates time-bound HMAC signatures on reordered payloads.
- Out-of-Order Financial Mutations: Processing a
refundbefore achargetriggers negative balance states or duplicate ledger entries. - Clock Skew Across Distributed Consumers: NTP drift causes sequence timeout windows to misfire, rejecting valid payloads.
Implementation Patterns & Security Controls
- Sequence-Bound Idempotency Keys: Combine
idempotency_key = f"{resource_id}:{seq_id}"to ensure retries are scoped to exact sequence positions. - State Reconciliation Pipelines: Implement periodic diff checks between event stream state and authoritative database state.
- Quorum-Based Commit Validation: Require acknowledgment from multiple consumer replicas before marking a sequence position as committed.
- HMAC Validation with Sequence-Aware Nonces: Include
seq_idin the HMAC payload to bind cryptographic integrity to ordering. - Strict TLS 1.3 Enforcement: Mandate forward secrecy and AEAD ciphers to prevent MITM reordering or payload injection.
Operational Workflows
- Automated Audit Trail Generation: Log every sequence validation attempt, rejection reason, and state mutation for forensic analysis.
- Rollback Procedures for Corrupted State: Maintain compensating event handlers that reverse mutations when out-of-order application is detected.
Runnable Implementation: Sequence-Bound Idempotency Middleware
import redis
class IdempotentSequenceProcessor:
def __init__(self, redis_client: redis.Redis, ttl_seconds: int = 86400):
self.redis = redis_client
self.ttl = ttl_seconds
def process(self, event_id: str, seq_id: int, handler_func) -> dict:
idempotency_key = f"webhook:seq:{event_id}:{seq_id}"
# Check if already processed
if self.redis.exists(idempotency_key):
return {"status": "duplicate", "idempotency_key": idempotency_key}
# Mark as in-progress to prevent concurrent processing
acquired = self.redis.set(idempotency_key, "processing", nx=True, ex=self.ttl)
if not acquired:
raise RuntimeError("Concurrent processing detected for sequence position")
try:
result = handler_func()
self.redis.set(idempotency_key, "completed", ex=self.ttl)
return {"status": "processed", "result": result}
except Exception as e:
self.redis.delete(idempotency_key)
raise e
Troubleshooting Steps
- Symptom:
Concurrent processing detectederrors spike during retries. Action: Implement distributed locks with exponential backoff. Ensurenx=Trueflag is set on RedisSETcommands. - Symptom: State corruption after out-of-order delivery.
Action: Enable optimistic concurrency control (version columns) in downstream databases. Reject writes if
expected_version != current_version. - Symptom: HMAC failures during regional failover. Action: Synchronize NTP across all consumer nodes. Use sequence-based nonces instead of timestamp-based nonces for cross-region deployments.
High-Stakes Sequencing and Compliance Requirements
Regulatory frameworks (PCI-DSS, SOX, GDPR) and financial auditing standards mandate strict, verifiable event ordering. Zero-tolerance sequencing environments require cryptographic proof of delivery sequence, immutable audit anchoring, and deterministic replay capabilities. For domain-specific compliance patterns and cryptographic sequencing implementations, consult Implementing strict ordering for financial webhooks. The choice of how strong a delivery contract to demand — and what it costs in throughput and complexity — is dissected in At-least-once vs exactly-once delivery trade-offs. What an auditor actually wants is not a log line claiming the order was correct, but a proof that the order could not have been altered after the fact — which is what folding each payload hash into its successor buys you.
Failure Mode Analysis
- Cross-Region Replication Delays: Asynchronous database replication violates ordering SLAs when consumers read from lagging replicas.
- Compliance Audit Failures: Unlogged sequence gaps trigger regulatory penalties during external audits.
- Out-of-Order Transaction Processing: Financial systems applying debits before credits violate accounting principles and trigger fraud alerts.
Implementation Patterns & Security Controls
- Cryptographic Hash Chaining: Hash each payload’s signature into the next payload’s
prev_hashfield, enabling cryptographic verification of sequence integrity without a full Merkle tree. - Multi-Region Leader Election for Dispatch: Use Raft/Paxos consensus to elect a single dispatch coordinator, ensuring global sequence generation.
- Immutable Audit Log Anchoring: Append sequence proofs to append-only logs (e.g., AWS QLDB) for regulatory compliance.
- Role-Based Access Controls for Sequence Override: Restrict manual sequence adjustments to audited, multi-approval workflows.
Operational Workflows
- Compliance Reporting Automation: Generate daily sequence integrity reports with cryptographic proofs for auditors.
- Cross-Region Sequence Synchronization Drills: Conduct quarterly failover tests to validate sequence continuity during region outages.
Runnable Implementation: Hash-Chain Sequence Proof
import hashlib
import json
from typing import List
class HashChainVerifier:
"""
Maintains a hash chain over ordered payloads.
Each entry is: SHA-256(prev_hash + SHA-256(canonical_payload)).
"""
def __init__(self):
self.chain: List[str] = []
def append_payload(self, payload: dict) -> str:
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
payload_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
if not self.chain:
self.chain.append(payload_hash)
else:
prev_hash = self.chain[-1]
combined = hashlib.sha256(
(prev_hash + payload_hash).encode("utf-8")
).hexdigest()
self.chain.append(combined)
return self.chain[-1]
def verify_sequence(self, expected_root: str) -> bool:
return bool(self.chain) and self.chain[-1] == expected_root
Troubleshooting Steps
- Symptom: Hash root mismatch during audit verification.
Action: Verify JSON canonicalization rules. Ensure all consumers use identical
sort_keys=Trueand separator configurations. - Symptom: Cross-region failover breaks sequence continuity. Action: Implement leader election with quorum writes. Ensure sequence counters are persisted to a strongly consistent datastore before dispatch.
- Symptom: Regulatory penalty for unlogged sequence gaps. Action: Deploy sidecar log aggregators that capture every sequence validation result. Implement automated gap reconciliation before compliance report generation.
Rolling Out Ordering to a Live Consumer
Enabling ordering on an integration that has been processing events in parallel is a capacity change disguised as a correctness change. The moment enforcement goes on, the consumer’s effective concurrency drops from “number of workers” to “number of partitions”, and if the partition count was chosen after the fact the queue depth starts growing during the deploy. Compute the requirement before the flag exists: partitions must be at least peak events per second multiplied by p99 service time, with 50% headroom. At 3,000 events per second and a 40 ms p99 that is 3,000 × 0.04 × 1.5 = 180 partitions, rounded up to 256 so the count stays a power of two and future consistent-hash changes stay cheap.
Sequence the rollout in four stages. First, stamp sequence numbers at the producer while every consumer ignores them; this alone is safe and gives you the number that decides whether the rest of the work is justified — the fraction of deliveries that actually arrive out of order, typically 0.5% to 3% for HTTP fan-out. Second, deploy a consumer that records violations but still applies every event, so you learn the real reorder-distance distribution and can size max_wait_ms from data rather than folklore. Third, enable enforcement for one event type on one tenant and watch four numbers: per-partition lag, buffer occupancy at p99, the rate at which gaps open, and the rate at which they time out. Fourth, widen by event type, never by traffic percentage — sampling splits a single resource’s stream across enforced and unenforced paths, which is the one configuration guaranteed to corrupt state.
The rollback rule is the counterintuitive part. Roll back by disabling enforcement, never by disabling cursor maintenance. A consumer that stops advancing its persisted cursor while continuing to process events ends up with a cursor pointing thousands of positions in the past, and re-enabling enforcement then makes every incoming event look early: all partitions buffer, all of them time out together, and the dead-letter queue absorbs a full max_wait_ms worth of traffic at once. Keep the cursor advancing on every applied event even in log-only mode, and re-enabling is a no-op rather than an incident.
Replay is the last thing to design and the one most often forgotten. Events pulled back from a dead-letter queue carry sequence numbers below the current cursor, so a strict consumer rejects them as stale — which is correct for live traffic and useless during recovery. Give the replay path an explicit mode that bypasses the cursor comparison but keeps the version or idempotency check, so out-of-band reprocessing can repair a gap without either corrupting the live cursor or double-applying. Test that path on the same schedule as the primary one; a replay tool that has never been exercised is discovered to be broken at exactly the moment it is needed.
Sequence Integrity Debugging Checklist
Run through these checks when consumers process events out of order or reject valid ones:
- Confirm events sharing a resource map to the same partition via consistent hashing (
hash(resource_id) % partitions). - Verify sequence IDs are generated centrally (DB sequence, Redis
INCR, Snowflake) — never client-side counters. - Check the reorder buffer’s
max_wait_msandmax_buffer_sizeagainst peak traffic to avoid premature flushes or overflow. - Ensure HMAC signatures bind
seq_idand raw bytes so reordered or replayed payloads fail verification. - Validate that out-of-order or timed-out payloads route to a dead-letter queue rather than corrupting state.
- Synchronize NTP across consumers so time-bound signature windows do not misfire during failover.
Frequently Asked Questions
Do HTTP webhooks ever preserve order without extra work?
No. Independent HTTP requests can be retried, load balanced, and queued separately, so two events dispatched a millisecond apart may arrive seconds apart in either order. Any ordering you observe in testing is an accident of low volume and will disappear the first time a delivery is retried.
What should the consumer do when a gap never fills?
Give up deliberately after a bounded wait rather than stalling forever. Route the buffered successors to a dead-letter queue, advance the cursor past the missing position, and alert — a stalled partition that blocks silently is far more damaging than a logged skip. If the producer publishes a high-water mark, use it to distinguish a sequence number that is late from one that was never issued.
How many partitions should a webhook consumer run?
Enough that the concurrency ceiling sits above your peak load: multiply peak arrival rate by p99 handler duration, add half again, and round up to a power of two. Strict ordering caps in-flight work at one event per partition, so the partition count is the concurrency ceiling regardless of how many workers are attached. Adding workers past that point changes nothing.
Can I avoid needing ordering altogether?
Often, yes. If every payload carries the full resource state plus a monotonic version, the consumer can apply an update only when the incoming version is higher, and the final state converges no matter what order events arrive in. This fails only for delta events, where applying the same changes in a different order genuinely produces a different result.
Why does a slow endpoint for one customer delay everyone else?
Because many keys share each partition and a partition processes one event at a time. With 50,000 tenants across 64 partitions, roughly 780 tenants sit behind any one slow consumer. Partition on a finer key and enforce a hard per-event timeout so a pathological handler costs bounded head-of-line time.
Is it safe to increase the partition count later?
Not with a plain modulo hash, which reassigns most keys the moment the divisor changes and strands their in-flight predecessors in buffers that will never receive successors. Use a consistent-hash ring or a stored key-to-partition map, and drain the old assignment before serving the new one.
Where should the sequence cursor be stored?
In the same transaction that applies the side effect, typically as a column on the resource row. An in-memory cursor resets on every deploy, and a consumer that restarts at zero treats all live traffic as early, buffers it, and dead-letters an entire partition one timeout later.