The Hidden Failure Mode in Serverless: Cold Start Isn't Your Biggest Problem
Cold start gets all the attention, but eventual consistency in event-driven serverless workflows is what really bites. Duplicate events, out-of-order updates, and silent data loss happen when handlers fan out and retry.
· 6 min read
Most serverless discussions fixate on cold start latency, invocation overhead, and the first request penalty. But those issues are measurable, bounded, and mostly solved by provisioned concurrency or runtime choice. The failure mode that actually sinks event-driven serverless systems is event consistency. When a single user action fans out into multiple events that trigger other events across queues, topics, and streams, you lose the transaction boundary. Handlers retry. Events arrive twice. Ordering is best-effort. Your database sees writes that never should have happened, and some writes that should have happened never do.
This is not theoretical. I have traced impossible inventory counts and doubled payments to exactly this: an event worker crashed after processing but before deleting the queue message, so the message redelivered. The handler ran again. No transaction, no idempotency key, no deduplication. Just a second write.
Why is eventual consistency a bigger problem in serverless than in a monolith?
In a monolithic app, a user request typically runs inside a single process with a database transaction. If the code throws, the transaction rolls back. Serverless breaks that model. You decompose the request into small functions, each with its own side effects, connected by an event bus or queue. The database transaction is gone. What you have instead is a choreography of at-least-once messages.
At-least-once delivery is the core assumption in most serverless event sources. SQS, SNS, EventBridge, Kafka, even DynamoDB Streams: the platform may deliver a message more than once, and it will not preserve order across partitions or shards. Your handler must be safe to execute multiple times with the same input. That is not the default. A naive Lambda that increments a counter or inserts a row will break the moment a message is retried.
What actually happens when an event is delivered twice?
You get duplicate processing. For a payment, that means charging a customer twice. For an inventory system, that means decrementing stock twice. For a notification, that means sending two emails. The fix is not to catch errors better. The fix is to design the event itself as an idempotency token, and to design the downstream writes so that repeats are harmless.
How does out-of-order delivery corrupt state?
If you have two events that mutate the same record, and they arrive in reverse order, the final state can be wrong. A common example: user changes their address, then changes it again. The later change is processed first, then the earlier one overwrites it. You have silently reverted to an old value. The only way to avoid this is to version your events, or to use a data store that supports last-write-wins with a timestamp or sequence number in the payload. But most serverless data stores do not give you that for free, and you have to build it into the event schema.
What causes silent data loss in event-driven workflows?
Silent data loss is not a crash. It is when the system drops a message without telling anyone. In serverless, this happens in a few predictable ways:
- Dead-letter queues that nobody monitors. You set up a DLQ for a function that fails too many times, but you never build an alert or a replay mechanism. The messages sit there until they expire. They are gone.
- Filtering and routing mistakes. An EventBridge rule with a wrong pattern silently discards events that should have triggered a function. You never see an error because the event was never delivered.
- Idempotency gone wrong. You store a processed event ID in a table, but the table write fails or the lookup is eventually consistent. The handler thinks the event is new, processes it again, or skips it entirely.
- Stream checkpoints too aggressive. A consumer that commits an offset before fully processing the batch will skip events on restart. If the function crashes between processing and checkpointing, you get duplicates; if it checkpoints early, you get loss.
The common thread: there is no transaction to roll back, and the error surfaces only as missing data later.
How can you design for exactly-once semantics without a database transaction?
Exactly-once processing is a myth at the infrastructure level. No queue, bus, or stream can guarantee that a handler runs exactly once for every event. But you can achieve effectively-once semantics by combining these patterns:
Make every handler idempotent with event identifiers
The most reliable approach is to assign each business event a unique, deterministic identifier at the source. For example, a payment intent ID or a user operation UUID. Then, in the handler, before doing any side effect, check that ID against a table or cache that records processed events. If it exists, return immediately. If it does not, write a placeholder with a condition expression (attribute_not_exists) to claim the event, then do the work, then mark it as completed. If the handler crashes after claiming but before completing, the next retry sees the placeholder and knows not to redo the side effect.
This works well in DynamoDB with strongly consistent reads or a conditional write. But it does not cover cases where the side effect is external, like charging a credit card. There, you need a two-phase pattern: insert a payment record with a status of "pending", call the processor, then update to "succeeded". On retry, check the status first. If it is "pending", call the processor again but pass the same idempotency key so the provider deduplicates. Most payment providers support idempotency keys for exactly this reason.
Use transaction outbox for multiple side effects
If a handler needs to write to two places, like a database and a message queue, there is no transaction that spans both. The classic solution is the outbox pattern: write the database changes and an "outbox" record in the same database transaction, then a separate process reads the outbox and publishes the events. This makes the publication eventually consistent with the database write, but it guarantees that every database change eventually produces an event, and no event is published without a database change. In a serverless context, you can implement this with DynamoDB streams: the stream is the outbox. The handler writes a single record with a status field, and a separate stream processor publishes events based on the record's state. This avoids a separate outbox table.
Version your events and store every write as an append-only log
For out-of-order problems, do not try to enforce ordering at the transport layer. Instead, make every handler accept events in any order and resolve conflicts using a version number or timestamp in the event. When you write to your data store, include a condition that the incoming version is greater than the stored version. If not, skip the write. This is optimistic concurrency, and it works in DynamoDB with ConditionExpression. For example, a user profile update event carries a version field. The handler writes only if version > :current_version. Old events become no-ops.
This approach means your data store is an append-only log of changes, and the current state is a projection. It is more work, but it eliminates the silent overwrite that comes from out-of-order delivery.
Treat retries as a feature, not a failure
Serverless platforms retry by default. Lambda retries twice for asynchronous invocations, SQS has a visibility timeout and redrive policy, Step Functions has built-in retry with backoff. The system is already leaning toward at-least-once. Your job is to make that safe. That means:
- Set a maximum retry count and a dead-letter queue.
- Build a replay endpoint or function that can pull from the DLQ and re-inject events after you fixed the bug.
- Monitor DLQ depth and set alarms. A DLQ with messages is a canary for a logic error, a downstream outage, or a data issue.
The hidden failure mode is not that events are duplicated. It is that you did not design for duplication. Eventually consistent event-driven workflows are the default in serverless, and they demand a different way of thinking about state and transactions. The developers who treat serverless as just a deployment target will ship bugs that only appear under retry storms or traffic spikes. The developers who design for idempotency, versioning, and outboxing can build systems that are actually more reliable than the monolith they replaced. I have seen the difference in production. It is real, and it is measurable.
If you are building a serverless system that processes payments or inventory, look at how I handled idempotency and outbox patterns in the Travel Booking Platform. The design is the same: event IDs, conditional writes, and a stream processor for outbox publication.
Keep reading
- webassemblyrust
WebAssembly Is Not Just for the Browser Anymore: Running Wasm on the Server with Rust and WASI
Server-side WebAssembly with Rust and WASI is challenging containers for edge and multi-tenant workloads. I dig into the real capabilities and the hard limitations you won't see in the hype.
- custom software costmalaysia
How Much Does Custom Software Actually Cost?
Custom software can cost anywhere from RM20,000 to RM500,000 or more. Here's a plain-language breakdown of development, maintenance, and hidden fees so you can decide if the investment pays off for your business.
- static site generatorsjamstack
The Unreasonable Effectiveness of Simple Static Site Generators in 2026
Forget the JAMstack hype cycle. Plain HTML generated from Markdown with a makefile still beats most modern stacks for docs, blogs, and internal tools. Here's why, and when to resist the urge to add a JavaScript framework.