What you'll learn: By the end of this lesson you will be able to describe the life of a function execution environment and what causes a cold start, calculate the concurrency a workload needs and say what happens when a limit is reached, explain why event delivery is at least once and design a handler that is safe to run twice, choose how to cope with event ordering, and decide when functions are the wrong tool.
A taxi is cheap if you ride twice a week and ruinous if you cross the city all day, and the first minute of every ride is spent waiting for the car to arrive. Now add a postal service that sometimes delivers the same letter twice, and never promises that letters arrive in the order they were sent. That is the shape of serverless, event-driven design: you pay for use, you get no idle cost, and you accept a wait, limits you do not control and messages you must treat carefully.
ShopStream stage 8 names two good fits (image resizing, webhooks) and one bad one (checkout), and message queues covers delivery semantics in general. This lesson covers what changes when the consumer is a function the platform starts for you.
Verified on 2026-11-08 against the AWS Lambda developer guide, the Cloud Run documentation (min instances, concurrency), the Cloud Run functions tips page, the CloudEvents specification page (it is titled version 1.0.3-wip; events carry specversion "1.0") and the Knative Serving autoscaling pages. Quotas and defaults change: read your provider's current page.
The core idea
Serverless means the platform starts, scales and reclaims the execution environments; event-driven means work begins because something happened, not because a caller waited. Put together, instances appear in response to events, and the platform may deliver an event more than once. That gives four design problems: the wait for a new environment (cold start), the cap on how many run at once (concurrency), duplicates (idempotency) and order.
Three illustrations run through the lesson: a minimal example (a handler and a duplicate-safe wrapper, both run), a realistic example (ShopStream photo thumbnails and payment webhooks) and a failure example (a backlog that exhausts a database).
The life of an execution environment
AWS's Lambda documentation describes three phases. In Init the platform starts extensions, bootstraps the runtime and runs your static code (everything outside the handler). In Invoke it runs the handler. In Shutdown it ends the environment. After an invocation the environment is frozen and kept for some time; a later request can reuse it (a warm start). The documentation also says Lambda terminates environments every few hours, even for functions invoked continuously, so you must not assume persistence.
Figure 1. Init happens once per environment, Invoke once per request. How to read this: a cold start is the path through Init before the first Invoke. Everything on the Frozen to Invoke loop is warm. A failed invocation sends the environment back through Init.
The minimal example shows why this matters. State outside the handler survives between warm invocations and nowhere else. This code, written in TypeScript and run here with Node 26 (it is not deployed anywhere), prints the first call as cold and the next two as warm:
// Code outside the handler runs once per execution environment (the Init phase).
let invocations = 0;
export const handler = async () => {
invocations += 1;
return { coldStart: invocations === 1 };
};
for (let i = 0; i < 3; i++) console.log(await handler());
{ coldStart: true }
{ coldStart: false }
{ coldStart: false }
That surviving state is a cache, not storage. AWS's design guidance says to assume the environment exists for a single invocation and commit permanent changes to a durable store before exiting. Google's Cloud Run functions tips agree: there is no guarantee that state is preserved, and background work after the function ends makes no progress.
Cold starts
The Lambda documentation describes a cold start as the service downloading your code, starting the environment and running initialisation code before the handler; you are charged for that time. It says cold starts typically occur in under 1 percent of invocations and range from under 100 ms to over 1 second, and are more common in rarely invoked development and test functions. The init phase is limited to 10 seconds, except with provisioned concurrency or SnapStart. Those are AWS's description of its own service at the time of writing, not a promise.
What lengthens Init is what you put there: package size and layers, the amount of initialisation work, and slow connection setup. What shortens a cold start:
- Do less in Init. Import only what you need and load rarely used objects lazily (the documentation and Google's tips both say this).
- Pre-initialise. Lambda's provisioned concurrency keeps a set number of environments initialised and costs extra; Cloud Run's minimum instances do the same job, and its page says they help most when scaling from zero.
- Keep it off the critical path. A cold start in an asynchronous worker hardly matters; on a shopper's request path it does.
Concurrency and the limits that shape a design
In Lambda, concurrency is the number of in-flight requests; each concurrent request needs its own environment, and an environment handles one request at a time. The documentation gives a formula you can use directly:
concurrency = average requests per second x average duration in seconds
For a ShopStream payment-webhook function (illustrative numbers: 200 webhooks per second, 250 ms each), that is 200 x 0.25 = 50 environments. Double the duration and you need 100.
Limits cap that number. At the time of writing the Lambda page says an account has a default limit of 1,000 concurrent executions per Region, that requests per second are limited to ten times the concurrency, and that throttled requests can be dropped. It describes two per-function controls: reserved concurrency (a maximum, and exclusive capacity no other function can use, which also stops a function using up an account's capacity or overloading downstream resources) and provisioned concurrency (pre-initialised environments, to cut cold starts). Cloud Run's concurrency page takes a different shape: one instance serves many requests, up to a configurable maximum (default 80 on the console for Cloud Run services), and concurrency 1 forces many instances to start during a spike. Knative Serving lets you set a soft target (default 100 per its documentation) that bursts can exceed and a hard limit (default 0, unlimited) that buffers the surplus.
The most common design consequence is a downstream one. Fifty environments that each hold a database connection are fifty connections (this follows from the documentation's description of connections kept in static initialisation, and is an inference about your database, not a stated figure).
Figure 2. Concurrency is a throttle you choose. How to read this: the queue absorbs the burst and the function drains it at a rate the database can take. On Lambda with SQS, the event source mapping has a maximum concurrency setting (2 to 1,000), and the documentation warns that if the function's reserved concurrency is lower than the total of those settings, messages can be throttled.
Delivery: at least once, and why handlers must be idempotent
Lambda's documentation is blunt. For asynchronous invocation, Lambda queues the event and returns success; if the function errors it retries twice by default, waiting one minute and then two; for throttling and system errors it keeps retrying for up to 6 hours with a backoff from 1 second to 5 minutes. It adds that the same event can arrive more than once even without an error, because the queue is eventually consistent, and that events may be dropped if the function cannot keep up, so you need enough concurrency. When events expire or fail, they are discarded unless you configured a dead-letter queue or an on-failure destination.
For an SQS source, the documentation says Lambda event source mappings process each event at least once and that duplicates can occur. A failed batch becomes visible again in full after the visibility timeout, successes included, unless you turn on partial batch responses and return only the failed message IDs. That is why every handler must be idempotent: running it twice must equal running it once.
CloudEvents helps name the duplicate. Its specification requires id, source, specversion and type, requires producers to make source plus id unique for each distinct event, says a re-sent duplicate may reuse the same id, and allows consumers to treat identical source and id as duplicates. The page defines the envelope, not delivery or ordering guarantees.
Figure 3. The claim must be atomic. How to read this: the store's "insert if absent" is the whole mechanism. A separate read then write would let two parallel deliveries both pass the check.
The same pattern, run in Node 26 with a stand-in in-memory store (a real one would be a database or a table with an atomic conditional write), gives:
type Event = { source: string; id: string; payload: string };
// A store with an atomic "insert if absent" is the only requirement.
const seen = new Set<string>();
const claim = (key: string): boolean => (seen.has(key) ? false : (seen.add(key), true));
let charges = 0;
export function handle(e: Event): string {
if (!claim(`${e.source}/${e.id}`)) return "duplicate, skipped";
charges += 1; // the side effect
return "processed";
}
const evt = { source: "payments-provider", id: "evt_42", payload: "paid" };
console.log(handle(evt));
console.log(handle(evt));
console.log("effects:", charges);
processed
duplicate, skipped
effects: 1
AWS suggests a DynamoDB table of recently processed identifiers with a time-to-live to limit its size. One gap: a handler that claims the key and then crashes before the effect completes has blocked its own retry. Real designs record a state (in progress, done) with an expiry, or make the effect and the claim commit together.
Ordering
Nothing in a plain function trigger orders events. Concurrency means two events for the same order can run at once, and retries mean an older one can arrive after a newer one. SQS FIFO queues give per-group ordering: messages with the same group ID are delivered to Lambda in order, a batch can contain several groups, and concurrent invocations are capped by the number of group IDs (six groups with a maximum concurrency of 10 give at most six). After a failure, Lambda retries the affected messages before taking more from that group, and the documentation says a function using partial batch responses on FIFO should stop at its first failure and return all failed and unprocessed messages.
| Strategy | How it handles order | Cost |
|---|---|---|
| Group by key in a FIFO queue | Order within one key (an order id) | Parallelism is limited to the number of keys; a stuck message holds its group |
| Version or sequence number in the event | Handler ignores an event older than the stored state | Producer must supply versions; see IDs, clocks and ordering |
| Commutative updates | Order stops mattering | Not every operation can be written that way |
Trade-offs
| Option | Gain | Cost | Wrong when |
|---|---|---|---|
| Functions on a managed platform | No idle cost, platform scaling | Cold starts, quotas, duplicates, platform coupling | Steady, latency-critical, stateful work |
| Containers on Kubernetes, scale to zero (Knative) | Same model on your cluster | You run the platform | A small team with no platform skill |
| Always-on services and queue workers | Predictable latency and cost | You pay for idle; you scale it | Highly bursty, rare work |
Figure 4. A decision, not a trend. How to read this: functions fit work that is event-driven, bursty, tolerant of a wait and safe to repeat. Anything that fails these tests is better as a service.
In practice: ShopStream thumbnails and payment webhooks
This is an educational reference design, not the verified internal architecture of any company. Numbers are illustrative. This is a realistic scenario, not a case history.
The situation
Stores upload product photos (illustrative: 5,000 stores, a morning burst of 40,000 uploads after a catalogue import). The payment provider sends status webhooks at unpredictable times, and retries when it does not get a quick success. Checkout is not part of this decision.
Starting architecture
Figure 5. Two function paths, each with a different safeguard. How to read this: thumbnails are bursty and idempotent by nature (the same input gives the same output file), so a cap and a queue are enough. Webhooks must not apply twice, so they get a dedupe claim before publishing.
Options considered
| Option | Behaviour | Cost | Risk |
|---|---|---|---|
| A. Image and webhook workers always on | Predictable | Idle cost between bursts | Over- or under-provisioned |
| B. Functions with caps and a dedupe claim | Scales to the burst | Quotas, cold starts | Duplicates, backlog behaviour |
| C. Functions for checkout too | One model | Cold starts on a shopper path | Latency and limits on the critical path |
The decision
Option B for these two paths, never C. The thumbnail queue gets a maximum concurrency below the database's connection budget. The webhook function verifies the signature, claims provider/event id atomically, enqueues the event and returns quickly; the order pipeline still handles it with its own idempotent consumers (order pipeline stage). Both queues get dead-letter queues that someone watches.
Rollout and verification
Replay sample webhooks with duplicates in a test environment and check the effect count equals the distinct event count. Load the thumbnail queue with a synthetic backlog and watch concurrency, throttles, queue age and database connections.
The 2 a.m. failure
The page reads: the thumbnail queue is 40,000 deep, errors are climbing and the database refuses connections. Checks: concurrency against the cap (was the maximum set on the event source mapping, or only on the function?), throttle counts, connection count at the database, and whether failed batches are returning whole (successful messages reprocessed, adding load). Mitigation: lower the concurrency cap, so the backlog drains slowly. Lasting fix: enable partial batch responses, keep the cap below the connection budget, and alarm on queue age, not just depth.
Architect's notes
- Assume at least once everywhere. Duplicates are normal, not rare.
- Cap concurrency to protect what is behind the function.
- Keep state out of the environment. It is a cache that disappears.
Common mistakes
- Treating a global variable as storage. It vanishes when the environment does.
- A read-then-write duplicate check. Parallel deliveries both pass it.
- Retries in the platform and in the code, each multiplying the other.
- No dead-letter queue, so failed events are discarded.
- Assuming FIFO order across all keys, when it holds only within a group.
Key takeaways
- An environment goes Init, Invoke, Frozen, and may be reused or shut down; cold starts are Init time on the request path.
- Concurrency equals request rate times duration; limits throttle and can drop requests.
- A concurrency cap is a design tool for protecting databases.
- Delivery is at least once: duplicates and out-of-order arrival are normal.
- Idempotency needs an atomic claim on an event's
sourceandid. - Order holds within a key, not across keys.
- Use functions for bursty, tolerant, repeat-safe work, not for steady latency-critical paths.
Practice exercises
Exercise 1 (30 min). A function takes 400 ms on average and receives 300 requests per second at peak. Compute the concurrency and compare it with a default limit of 1,000.
Exercise 2 (45 min). Write the claim-then-act flow for a handler that must send one email per source/id, including what happens if it crashes between claim and send.
Exercise 3 (1 hour). Design the ordering of order.created, order.paid and order.cancelled events for one order, using a group key and a version field.
Exercise 4 (2 hours). Write a decision record choosing functions or containers for a thumbnail service.
The other side of this topic
On the front end, event-driven design shows up as UI that reacts to server pushes and local events instead of polling, with the same questions about duplicates and out-of-order updates. The front-end track covers it as event-driven UI architecture.
Related reading
- ShopStream Stage 8: services: where functions fit, and where they do not.
- Message Queues: delivery semantics and dead-letter queues.
- Idempotent and Commutative Operations: safe repeats.
- Event Streaming: ordered logs for events.
- Resilience Patterns: retries and backoff.
Official documentation
- AWS Lambda: Execution environment lifecycle and Scaling and concurrency.
- AWS Lambda: Asynchronous error handling, SQS event sources and designing applications.
- Google Cloud: Cloud Run concurrency and Cloud Run functions tips.
- CloudEvents specification.
- Knative: Autoscaling and Concurrency.