The serverless decision record: why the compute model you chose determines your cold start surface and your vendor dependency ceiling

Published 2026-07-13 · WhyChose

Serverless decisions are made when the team needs to run code without managing servers and does not want to pay for always-on compute during periods of zero or low traffic. The AI session that produces the serverless decision is practical and well-reasoned: it evaluates whether the team should use AWS Lambda, Google Cloud Functions, Cloud Run, or Azure Functions; selects the platform with the best fit for the team's cloud provider, existing IAM model, and trigger requirements; deploys the first function with the first event source trigger; and documents why serverless is the correct compute model for this workload class. The session produces a working function, a deployment pipeline, and a trigger configuration. It confirms that the billing model — per-invocation and per-GB-second — is more cost-effective than an always-on EC2 instance at the team's current request volume. The session is correct and the decision produces the intended outcome: the team deploys code without managing server infrastructure, pays only for what they use, and the first function handles its trigger correctly.

What the AI session does not produce is the serverless system's second half. The session answers "which FaaS platform and for which workloads?" and deploys the first function. It does not ask: what is the cold start budget for synchronous, user-visible invocations — the maximum latency penalty that a first invocation after a quiet period adds to the user-facing endpoint's response time — and what package size, initialization code constraints, and VPC attachment policy does that budget imply? What is the execution duration classification for each workload type, and what is the policy for re-evaluating a workload's placement on Lambda when its p99 execution duration approaches the 15-minute ceiling as the dataset it processes grows over the next 18 months? What is the inventory of Lambda-specific APIs and trigger event schemas used across the function fleet, and what is the exit cost estimate for migrating to a container-based alternative if Lambda pricing or constraints become unfavorable, and at what business trigger is that estimate re-evaluated? What are the retry semantics and DLQ configuration for async invocations, and which handler implementations are required to be idempotent because at-least-once delivery is guaranteed? Each of these questions has an answer that is not derivable from "we use Lambda" as the team frames the platform choice. Each answer determines whether the serverless compute model remains the correct infrastructure choice as workloads grow, or becomes the ceiling on latency, the source of production timeout incidents, and the anchor to a vendor coupling surface whose exit cost was never estimated before it became the dominant migration concern. The answers exist in the AI sessions. They are the operational commitments behind the compute model choice. They are almost never written down.

Two ways serverless decisions produce the wrong outcome at scale

The cold start latency ceiling

A fintech startup adopts Lambda for their transaction enrichment pipeline in the first year. The decision is well-founded: the enrichment service processes incoming transactions, appends metadata from third-party data providers, and writes the enriched record to the primary database. At founding time, the service handles 200–800 transactions per hour with occasional bursts, which makes the per-invocation billing model substantially cheaper than an always-on service, and the burst handling — Lambda scales to concurrent executions automatically — eliminates the capacity planning problem for a team of six engineers who need to move fast on core product work. The first function is 12 MB, cold starts take 280–410 ms (within the team's informal sense of "fast enough"), and the function handles its trigger correctly from day one. The session that produced this decision is correct in its analysis of the compute model and billing trade-off for the workload at its current scale.

Over the next twenty-six months, the enrichment pipeline's function package grows from 12 MB to 247 MB. The growth is incremental and individually reasonable: month three adds a rules engine library for configurable enrichment logic (18 MB); month seven adds a natural language processing library for merchant name normalization (31 MB); month fourteen adds scikit-learn and a custom feature extraction module for a fraud detection scoring model integrated into the enrichment step (68 MB for the library, 29 MB for the bundled model artifact); month twenty-two adds a financial data provider's SDK that bundles its own cryptographic dependencies rather than using the AWS SDK's already-present versions (34 MB). Each addition is reviewed and merged without reference to a cold start budget, because no cold start budget was established in the founding session. The function is also VPC-attached: in month four, the team moved the primary database from a publicly accessible RDS endpoint to a private subnet, requiring the Lambda function to run inside the VPC to maintain database connectivity. VPC attachment is a configuration change that takes ten minutes to apply and requires no architectural decision — it is the obvious solution to "Lambda can no longer reach the database."

In month twenty-seven, the team's SLA monitoring system generates a critical alert: the transaction enrichment endpoint's p99 response time has exceeded the three-second SLA threshold committed to enterprise customers in their service agreements. The APM trace shows that 94% of the p99 latency is in the enrichment Lambda function. The team identifies the problem within two hours: cold start latency. A 247 MB package in a VPC-attached function has a cold start time of 4.2–7.8 seconds, variably, depending on Lambda's execution environment provisioning conditions. The median warm invocation is 340 ms — well within the SLA — but any invocation that hits a cold execution environment, which occurs during traffic bursts, after a quiet period, or when Lambda provisioning shifts the function to a new execution environment during sustained high concurrency, produces a user-visible response time between 4.5 and 8.1 seconds that breaks the SLA guarantee.

The team evaluates three remediation paths. Package size reduction: stripping unnecessary dependencies, replacing scikit-learn with a lighter ONNX inference runtime, and removing the bundled model artifact from the package in favor of loading it from S3 at warm startup. Estimated reduction: from 247 MB to 85–110 MB, estimated cold start improvement: 1.8–2.6 seconds, not sufficient to bring cold starts below the SLA threshold for a VPC-attached function. Provisioned concurrency: configuring Lambda to keep a minimum number of execution environments initialized, eliminating cold starts for invocations served by the provisioned environments. At 4 provisioned concurrent executions (sized to handle the traffic burst pattern from Caddy logs) and the function's 4,096 MB memory configuration, provisioned concurrency costs $0.015 per GB-second × 4 GB × 3,600 seconds × 730 hours = $631 per month. The team's previous Lambda bill for this function averaged $74 per month on-demand. Architecture change: extract the fraud detection scoring model into a separate warm service (a container on ECS Fargate or a dedicated inference endpoint) and reduce the enrichment function's package to the lightweight enrichment logic that does not require ML dependencies, reducing package size below the cold start budget's threshold. Estimated engineering effort: four weeks. The team chooses provisioned concurrency as the immediate remediation and schedules the architecture change for the next quarter. The provisioned concurrency cost is approved as a budget exception because the SLA breach risk is immediate. The team's infrastructure cost model — which assumed Lambda's on-demand billing — does not have a line item for provisioned concurrency overhead because the founding session that selected Lambda did not define the conditions under which provisioned concurrency would be required. The $631/month is a surprise in the budget review that month, not a planned infrastructure cost.

The founding session that selected Lambda and VPC-attached the function did not address: what is the cold start budget for synchronous, user-visible invocations? What package size implies cold start latency above that budget? What is the review requirement for adding dependencies that increase the package size above the cold start threshold? What is the policy for VPC attachment — should functions that require database access be refactored to use the database's public endpoint through a NAT gateway (at NAT gateway data processing cost) or VPC-attached (at cold start cost), and is that trade-off documented so future engineers understand why the VPC attachment was chosen and under what conditions it should be reversed? Two years of individually-reasonable dependency additions and a pragmatic VPC attachment decision produced a 247 MB package with 4–8 second cold starts and a $631/month provisioned concurrency bill that the team would have addressed at package size 80 MB if a cold start budget had established that threshold at the time of each addition.

The execution duration timeout incident

A developer tools startup builds their report generation service on Lambda from their second year. The decision is correct and well-considered: report generation is a batch operation that runs on user request, processes transaction records from the primary database, applies configurable aggregation logic, and produces a downloadable report file. At the time of the decision, the largest customer's dataset spans eight months of transactions and the largest report takes 3.4 minutes to generate — well within Lambda's 15-minute execution duration limit. The session that produced this decision explicitly evaluated the duration ceiling: "Lambda's 15-minute limit is more than sufficient for our current report sizes and well above the 3–4 minute range we see for the largest reports." The session is correct about the current state. It does not define a workload duration growth policy: what happens when the largest report's generation time approaches the 15-minute limit, at what proximity to the limit does the team re-evaluate the compute placement, and what are the migration options and their engineering cost estimates?

Eighteen months after the Lambda-based report generation service is deployed, the startup closes its first enterprise contract. The enterprise customer has 14 months of transaction history at the time of contract signing and expects to generate quarterly reports across their full history. The first quarterly report request processes 14 months of transactions at the enterprise customer's transaction volume — 4.7× the startup's largest existing customer by transaction count. The report generation function is invoked with the enterprise customer's query parameters at 9:14am on a Tuesday. At 9:29am, sixteen minutes later, the Lambda runtime returns an error: Task timed out after 900.00 seconds. The customer receives a generic error response. The function's CloudWatch logs show that it completed approximately 73% of the required aggregation before timing out. There is no partial output, no progress indicator, and no mechanism for resuming from the 73% completion point — the function either completes or times out and the customer must retry from the beginning.

The team evaluates the migration options the same morning. Option one: Step Functions orchestration. The report generation is refactored as a series of Lambda invocations — one per month of data, with Step Functions passing intermediate results through S3 — allowing the total computation to span multiple invocations within Lambda's duration limit. Engineering estimate: three weeks to redesign the state model, implement the chunked invocation pattern, handle partial failures (a single month's invocation failing midway through the orchestrated flow), and test the result against the enterprise customer's full dataset. Option two: migrate report generation to ECS Fargate. A container-based task with no execution duration ceiling, configurable vCPU and memory, and the same IAM role and VPC access as the existing Lambda function. Engineering estimate: four weeks to containerize the report generation logic, create the Fargate task definition, integrate with the application's job scheduling system, and test. Option three: scope the report to a rolling 12-month window, capping the report at a size that Lambda can generate within 15 minutes. The enterprise customer's contract does not specify a date-range limit on reports; implementing the cap requires a contract negotiation and potentially breaks the enterprise deal. The team implements Step Functions in three weeks, adds enterprise-tier report scope monitoring to detect when other customers approach the duration ceiling, and documents the workload duration growth policy that the founding session did not produce. The policy is written after the incident rather than before it. The three weeks of unplanned engineering work is not in the quarter's roadmap — the team delays a planned feature to absorb the migration cost.

The founding session that selected Lambda for report generation evaluated the duration ceiling against the current maximum report size and found it acceptable. It did not define: what is the execution duration proximity threshold at which the team initiates a migration evaluation — 70% of the configured timeout at p99, 85%, or a specific duration in seconds? Which workload categories are appropriate for Lambda's duration model (bounded, user-triggered operations with predictable input size), which are marginal (batch processing with growing dataset coverage), and which should not be on Lambda from the beginning (unbounded streaming, ML training, large-file ETL)? What is the engineering cost estimate for the two most likely migration targets (Step Functions, Fargate) so that the cost is known before it is urgently required? How does the team monitor p99 execution duration per Lambda function and alert when a function's p99 exceeds the proximity threshold? Three questions, each answerable in the founding session, whose answers would have converted the three-week emergency migration from an unplanned incident response into a planned migration that had been on the roadmap for six months.

Three structural properties that serverless decisions determine

The cold start model and the latency surface

A Lambda function's cold start latency is the sum of four components, each of which is a policy-addressable cost. The runtime baseline — the time for Lambda to initialize the language runtime in a new execution environment — is largely fixed by the runtime choice: JVM-based runtimes (Java, Scala, Kotlin) carry a 1.5–4 second baseline because the JVM's class loading and JIT compilation execute at startup; interpreted runtimes (Node.js, Python) carry a 100–500ms baseline; compiled runtimes (Go, Rust via custom runtime) carry a 10–100ms baseline. The runtime choice is documented in the serverless decision record and rarely reconsidered. The package size contribution — the time Lambda spends extracting and loading the deployment package in the new execution environment — scales linearly with package size and is the component most subject to policy control through a package size budget. The initialization code contribution — code that executes outside the handler at module scope, including SDK client construction, connection pool creation, and configuration loading from environment variables or AWS Secrets Manager — is under complete team control through code review policy that identifies module-scope initialization and measures its cost. The VPC attachment contribution — eliminated for non-VPC functions, variable (1–3 seconds in modern VPCs using Hyperplane-based ENI management, up to 15+ seconds in older VPC configurations that allocated ENIs per-invocation) for functions that require private subnet access — is a configuration decision whose cost must be weighed against the alternatives at the time of each VPC attachment decision.

The cold start budget is the ceiling that makes each component's policy addressable. Without a budget, each component's contribution is not clearly a problem until the total crosses a threshold that produces an observable SLA breach. With a budget, package size additions that would push the total above the budget are identified at PR review time, initialization code that exceeds a per-client construction limit is flagged before it is deployed, and VPC attachment decisions are made against the knowledge that VPC attachment adds a fixed cold start cost that must fit within the remaining budget. The budget must be defined per trigger type because the acceptable cold start latency differs by invocation context: synchronous API-path invocations have user-visible cold starts and typically require a budget of 500ms–1 second above the warm median to stay within a 2–3 second total p99 target; async queue-triggered invocations have cold starts that add to message processing latency without user visibility and can tolerate a budget of 2–5 seconds; scheduled event-triggered functions have cold starts that affect the first invocation after a scheduled gap and can tolerate whatever budget the scheduled interval permits. The budget must also specify the provisioned concurrency decision criterion: when the package size reduction and initialization code optimization options are exhausted and cold start latency remains above the budget, provisioned concurrency is the remaining mitigation, and the team should know the cost of provisioned concurrency at the expected minimum concurrent execution count before deploying a function that will require it.

The package size budget is the most actionable policy artifact from the cold start model. A documented maximum package size — including all Lambda layers — enforced as a deployment check (a CI step that measures the deployment package size and fails the build above the threshold) converts package size growth from a silent accumulation problem into a decision: adding a dependency that would exceed the budget requires either removing another dependency or documenting a deliberate exception to the budget with a cold start cost acceptance. Lambda layers complicate the size calculation: a function's effective package size for cold start purposes includes both the function's own deployment package and all layers the function uses, which share the 250 MB unzipped limit. A function that uses a shared layer for common utilities and a shared layer for AWS SDK clients may have a function package of 30 MB but an effective deployed size of 180 MB including the layers. The package size budget must account for the total of function package and all attached layers, not the function package in isolation.

The execution duration limit and the workload classification ceiling

Lambda's 15-minute maximum execution duration is a hard ceiling, not a soft limit. A function invocation that reaches 900 seconds receives a forced termination with no cleanup opportunity and no mechanism for partial output recovery. The ceiling's practical consequence is not that 15 minutes is often insufficient — for most workloads at founding scale, it is more than sufficient — but that the ceiling creates a class of production failures that are invisible until the workload grows beyond a threshold that the founding session evaluated against the workload at its current size rather than at its expected size after 18–36 months of user and data growth. The timeout failure mode is particularly disruptive because it surfaces as a generic error to the user (Lambda's Task timed out after 900.00 seconds error message is not actionable for a user who requested a report), it leaves the workload at an unknown partial completion state with no recovery path, and it requires engineering effort to migrate the workload to a compute model without a duration ceiling, which is always more urgent than planned migration work because it is discovered through a production failure rather than through a monitoring alert.

The workload classification policy is the document that makes the ceiling's implications visible before it is encountered in production. Classification divides workloads into three categories based on their duration profile. The first category is bounded workloads: workloads whose execution duration is determined by the input size and is bounded by a contract or architectural invariant, not by the volume of data that accumulates over time. An API-triggered function that processes a single user action — validates input, applies business logic, writes a record — is bounded: its duration is proportional to the input request, which is bounded by the API's request size limit. An event-triggered function that processes a single message from a queue is bounded: its duration is proportional to the message payload, which is bounded by the queue's message size limit. These workloads are appropriate for Lambda without a duration growth monitoring requirement because their duration ceiling is stable. The second category is growth-bounded workloads: workloads whose execution duration grows with dataset volume or user-generated content that accumulates over time. Report generation functions that process a user's full history are growth-bounded; the duration grows as the user's data volume grows. Background jobs that process all records added since the last run are growth-bounded; their duration grows as the update rate increases with user count. These workloads require duration proximity monitoring from deployment: a CloudWatch alarm on the p99 execution duration that alerts when it exceeds a configured threshold — commonly 70–80% of the function's configured timeout — triggering a migration evaluation before the ceiling is exceeded in production. The third category is unbounded workloads: workloads with no architectural bound on execution duration, including streaming data processing, ML model training, and long-running background workers. These workloads are not appropriate for Lambda from the beginning and should be documented as such in the classification policy so that future engineers adding workloads to the system understand which compute model applies to each workload class without making the Lambda-or-container decision individually for each new workload.

The migration evaluation that the proximity alert triggers must be precomputed, not initiated for the first time at the alert. The two most common migration targets — Step Functions orchestration of chunked Lambda invocations, and ECS Fargate container tasks — each have an engineering cost estimate that should be recorded in the serverless decision record at the time the classification policy is written, while the functions are working correctly and the migration is not urgent. A migration cost estimate written under time pressure, after a production timeout incident, is less reliable than one written during a calm planning session by engineers who have just finished reading the current function's code for the purposes of writing the cost estimate. The decision record's migration evaluation section should document: the estimated engineering effort to migrate each growth-bounded workload to Step Functions, the estimated effort to migrate to Fargate, the estimated cost difference between the Lambda-based implementation and the migration target at the current data volume, and the data volume (or p99 duration) trigger at which the migration's cost advantage justifies its engineering cost.

The vendor coupling surface and the lock-in exit criteria

Lambda's vendor coupling surface accumulates through normal feature development: each new trigger type adds a dependency on that trigger's AWS-specific event schema; each Lambda-specific API call in handler code (reading from the Lambda context, using Lambda Powertools, writing structured logs in Lambda's expected format for CloudWatch Logs Insights) adds a coupling point; each Lambda layer adds a packaging dependency that has no direct equivalent in container-based compute. The coupling is not inherently wrong — Lambda's trigger integrations are one of its primary advantages over container-based alternatives — but it creates a migration cost that grows with each new trigger type and each new handler that uses Lambda-specific APIs, and that cost should be known and consciously accepted at the decision point, not discovered for the first time during a migration evaluation triggered by a cost or capability concern.

The vendor coupling inventory is an enumeration of the Lambda-specific API surface used across the deployed function fleet. The inventory has five categories. The first is trigger event schemas: each trigger type the team uses (S3, SQS, SNS, DynamoDB Streams, API Gateway, EventBridge, Lambda function URL, Kinesis, ALB, Cognito, Lex, Alexa, CloudFormation, CloudWatch Events) adds a schema dependency. Functions that abstract the trigger event — parsing the Lambda-specific event structure in a thin adapter at the top of the handler and passing a normalized internal event to the business logic — have a lower migration cost than functions whose business logic reads directly from the Lambda event object's AWS-specific field names. The second is the Lambda context API: context.getRemainingTimeInMillis(), context.awsRequestId, context.functionName, context.invokedFunctionArn. Functions that read these values in business logic (using getRemainingTimeInMillis() to implement early termination before the timeout) require adaptation on migration. The third is Lambda Powertools: the AWS Lambda Powertools library (available for Python, TypeScript, Java, .NET) provides structured logging, tracing with X-Ray, metrics with CloudWatch, and utility functions (idempotency check with DynamoDB, batch processing, feature flags via AppConfig) that are Lambda-specific integrations. Functions using Lambda Powertools' idempotency utility or X-Ray tracing have coupling to AWS-specific observability infrastructure. The fourth is Lambda layers: dependencies shared across multiple functions as Lambda layers must be converted to container build steps or shared library imports during a migration. The fifth is Lambda@Edge and CloudFront Functions: these are not portable at all; functions deployed to Lambda@Edge are deeply integrated with CloudFront's request and response lifecycle and have no equivalent in non-AWS CDN infrastructure.

The exit criteria are the business conditions that trigger a migration evaluation. The criteria should be specific and measurable, not vague ("if Lambda becomes too expensive"). Three criteria cover the most common triggers: a cost trigger (when Lambda's monthly cost for functions that could reasonably run on an alternative platform exceeds N times the estimated cost of the equivalent Fargate or Cloud Run compute, initiate a migration cost-benefit analysis within 30 days); a capability trigger (when a workload class cannot be served within Lambda's constraints — duration, memory, network, packaging — and Step Functions orchestration is not a viable solution, evaluate container alternatives for that workload class); and a strategic trigger (a material change in AWS Lambda pricing, a new compliance requirement that Lambda's execution environment does not support, or an organizational decision to migrate from AWS to a different cloud provider). Documenting the exit criteria converts the migration decision from a judgment call made under pressure to a policy-governed evaluation initiated by a measurable condition. The infrastructure architecture decision record is the parent document: the cloud provider choice determines which FaaS platforms are available without cross-cloud complexity, and the exit criteria for the serverless decision must align with the exit criteria for the cloud provider itself — a migration from Lambda to Cloud Run requires a cloud provider migration if the team is AWS-only, but is a same-tier compute shift if the team already uses multi-cloud.

Three AI session types that embed serverless decisions without documenting them

The infrastructure platform selection session is where the FaaS platform is chosen and the first function is deployed. The session evaluates the compute alternatives — Lambda versus Cloud Run versus Azure Functions versus a container on ECS Fargate — and produces a well-reasoned choice. The session notes that Lambda's per-invocation billing is cheaper at the current request volume, that Lambda's auto-scaling eliminates the capacity planning overhead, and that the team's existing AWS infrastructure (IAM roles, VPC, S3, RDS) makes Lambda the lowest-friction integration path. The session deploys the first function with the first trigger and verifies that it works correctly. What the session does not produce is the cold start budget (which requires deciding what latency is acceptable before the function's package size has grown), the execution duration classification (which requires evaluating each planned workload class against the 15-minute ceiling), the vendor coupling inventory (which requires enumerating the Lambda-specific APIs the function will use), or the exit criteria (which requires defining the conditions under which the platform choice would be re-evaluated). Each of these is a second-order question that the session frames as "we can address this if it becomes a problem," which is the correct framing for a team that needs to move fast and does not yet have the data to define the budgets precisely. The problem is not that the session defers these questions — deferring them is reasonable — but that it defers them without scheduling when they will be answered and without writing down that they exist as open questions. The open-source extractor surfaces these founding infrastructure platform sessions from AI chat history, recovering the FaaS platform selection rationale, the trigger integration decisions, and the compute model analysis that make visible which operational policies were implicit in the founding team's shared assumptions and which were never addressed.

The background processing architecture session is where async Lambda invocations are introduced for the first time. The session addresses a specific architectural problem — moving a slow or resource-intensive operation off the synchronous request path by triggering a Lambda function asynchronously via SQS, SNS, or EventBridge — and produces a working async architecture: a queue trigger configuration, a Lambda event source mapping with the correct batch size, and handler code that processes the async event. The session focuses on the architectural correctness of the async pattern (decoupling the producer from the consumer, enabling parallel processing, smoothing traffic spikes) and the implementation of the handler's business logic. What the session typically does not produce is the retry and DLQ policy: for SQS-triggered functions with the default Lambda event source mapping configuration, a handler that throws an exception causes the entire batch to be retried, with each retry consuming the message's visibility timeout until the message's maximum receive count is reached and it is moved to the DLQ. If the handler is processing a batch of 10 messages and one message's business logic is permanently invalid (a malformed record that will always fail validation), the default retry behavior retries all 10 messages for each retry attempt, successfully processing the 9 valid messages each time and failing on the 1 invalid message each time, until the invalid message's receive count is exhausted and the 9 valid messages have been processed 5 times each (at the default 5 retry maximum). The idempotency requirement — that processing the same message multiple times produces the same outcome as processing it once — is the policy that makes the retry behavior safe rather than dangerous. Without the idempotency policy, retry-driven reprocessing produces duplicate side effects: duplicate database records, duplicate emails, duplicate payment requests, or duplicate external API calls that the external system may interpret as separate events. The queue and messaging decision record is the companion document: the queue configuration (visibility timeout, maximum receive count, DLQ ARN), the batch processing behavior (batch item failure reporting, partial batch success handling), and the idempotency check implementation are each aspects of the messaging system and the compute model that must be documented together to produce a coherent async invocation policy.

The cost optimization session is where Lambda's billing model is first investigated as a cost driver rather than a cost advantage. The session is triggered by an AWS bill that has grown above the team's expectations, typically because function invocation volume has grown with user growth, or because a specific function's memory configuration is higher than its actual memory usage, or because a function is being invoked unnecessarily by a misconfigured trigger. The session correctly addresses the immediate cost driver: right-sizing function memory to the actual peak usage plus a safety margin (reducing memory from the provisioned 3,008 MB to the measured peak of 880 MB plus 30% = 1,144 MB reduces cost by 62% for every invocation of that function), consolidating multiple low-invocation-count functions into a smaller number of higher-invocation-count functions to reduce the fixed cold start overhead amortized across each function's invocation volume, or reducing the trigger's invocation frequency by batching events. The session addresses the specific cost problem it was initiated for. What it does not produce is a systematic function cost model: a spreadsheet or document that maps each deployed function to its monthly cost at the current invocation volume and memory configuration, which functions are approaching a cost level that justifies migration to always-on compute (a container that runs continuously may be cheaper than a Lambda function invoked at high frequency once the per-invocation cost exceeds the container's hourly rate times the container's minimum reservation), and which functions have provisioned concurrency that was added as an emergency remediation and has never been re-evaluated against the alternative of package size reduction. The cost optimization session creates a lower bill and a git commit. It does not create a cost model that the next engineer can use to evaluate the next new function's compute placement. The cloud cost optimization decision record must include the Lambda-specific cost model as a section: the billing dimensions (invocation count, GB-seconds, provisioned concurrency hours, data transfer), the break-even analysis between Lambda on-demand and always-on container compute at different invocation frequencies, and the cost review cadence that ensures each function's compute placement is re-evaluated as invocation volume changes.

The five sections of a serverless decision record

The first section documents the FaaS platform selection and compute model rationale. The selection rationale must be specific and evaluatable: "AWS Lambda was selected because the team's infrastructure is entirely AWS-based (IAM, VPC, S3, RDS), Lambda's per-invocation billing model is cheaper than ECS Fargate at fewer than 15,000 invocations per day, and the team's background processing workloads are triggered by S3 events and SQS messages, which Lambda's event source mappings handle natively without additional infrastructure" is evaluatable against "have our invocation volumes crossed the Lambda-to-Fargate cost break-even at our current memory configurations?" The compute model rationale must document the workload classes that were evaluated as appropriate for Lambda (user-triggered async operations, S3 event processing, queue consumption), the workload classes that were considered and rejected for Lambda (ML training, report generation beyond 10 minutes, streaming data processing), and the workload classes that were explicitly deferred as "evaluate at the time of first implementation" (report generation at enterprise scale, long-running background jobs added in future quarters). The evaluation of Lambda versus Cloud Run or Azure Functions must document what made Lambda the preferred choice over the alternatives, because a future engineer evaluating whether to migrate the fleet to Cloud Run needs to understand what the original trade-offs were before deciding whether they still apply. The section must also document the function organizational model: whether functions are deployed as a monorepo with a shared deployment pipeline, as independent repositories per function group, or as a SAM/CDK application with all functions in a single infrastructure-as-code template, because the organizational model determines how the package size budget and the cold start policy are enforced across the function fleet.

The second section documents the cold start policy. The package size budget section must specify the maximum allowable deployment package size (including all Lambda layers) per trigger type category: synchronous API-path functions have the tightest budget (commonly 50–100 MB to stay within a 500ms cold start contribution from package loading at the JVM or interpreted runtime's loading rate); async queue-triggered functions have a looser budget (150–200 MB); scheduled or background functions have the loosest budget (250 MB maximum, which is the Lambda deployment limit). The enforcement mechanism must be documented: a CI step that measures the deployment package size and fails the build above the threshold, a deployment check in the SAM or CDK template that validates each function's layer configuration against the budget, or a manual review requirement in the PR checklist for any PR that modifies a function's dependencies. The initialization code requirements section must specify the maximum allowable module-scope initialization for synchronous functions: commonly, no synchronous network calls at module scope (API calls to Secrets Manager, Parameter Store, or external services must be deferred to handler invocation time or handled via async module-level initialization that does not block the module load), and a measured initialization time ceiling verified by a cold start latency test in the CI pipeline. The VPC attachment policy must specify the conditions under which VPC attachment is required (database access via private subnet, internal service-to-service communication on a private network) and the conditions under which it should be avoided (public-endpoint access, functions that can use NAT gateway for private resource access), with the cold start cost of VPC attachment documented at the team's VPC configuration type (Hyperplane-based: 1–3 second overhead; legacy ENI-per-invocation: 10–15 second overhead). The provisioned concurrency decision criteria must specify: at what cold start latency does provisioned concurrency become required, what is the calculation for the minimum provisioned concurrency count for each function that requires it, and who approves the provisioned concurrency cost (since it changes the function's billing model from on-demand to reserved). The CI/CD pipeline decision record is coupled to the cold start policy: the CI pipeline must enforce the package size budget, run cold start latency tests for synchronous functions, and validate the provisioned concurrency configuration against the documented criteria before a deployment that modifies a function's configuration is promoted to production.

The third section documents the execution duration classification. The workload classification section must categorize each deployed function (and each planned future function class) into one of the three duration categories: bounded (appropriate for Lambda without growth monitoring), growth-bounded (requires duration proximity monitoring from deployment), or unbounded (not appropriate for Lambda). For growth-bounded workloads, the classification must specify the configured timeout per function (shorter configured timeouts than the 15-minute maximum are appropriate for bounded workloads — a function that should never take more than 30 seconds should be configured with a 60-second timeout, not a 900-second timeout, so that a runaway invocation fails fast rather than consuming Lambda concurrency for 15 minutes), and the proximity alert threshold at which a migration evaluation is initiated. The migration evaluation pre-computation section must document the Step Functions orchestration design for each growth-bounded workload: what the chunk boundary is (how the input is divided across invocations), how intermediate state is passed (through S3 for large payloads, through Step Functions state for small ones), how partial failures are handled (which steps are retriable, which require human intervention), and the engineering estimate for implementing the orchestration. The same pre-computation for ECS Fargate: the container image requirements, the task definition parameters (vCPU, memory, networking), the integration with the current application's job scheduling system, and the engineering estimate. These estimates exist in the decision record so that when the proximity alert fires, the migration evaluation is a decision between two known options with known costs, not a research project conducted under time pressure. The duration monitoring section must specify the CloudWatch alarm configuration: alarm on P99(Duration) per function exceeding 70% of the function's configured timeout, with the alarm evaluation period (15 minutes, to avoid false positives from isolated slow invocations) and the notification routing (to the team's alerting channel, not to PagerDuty, since this is a planning trigger rather than a production incident).

The fourth section documents the vendor coupling surface. The trigger event schema inventory must enumerate each trigger type in use and the Lambda-specific event schema fields that handler code reads from the event object. For each trigger type, the inventory must note whether the handler abstracts the event through a normalization adapter (lower migration cost) or reads the trigger-specific fields directly in business logic (higher migration cost). The Lambda context API usage inventory must note every handler that reads from the Lambda context object — particularly getRemainingTimeInMillis(), which is used for early termination logic, and awsRequestId, which may be used as a correlation ID in logs. The Lambda Powertools usage inventory must list each Powertools utility in use: the idempotency utility's DynamoDB table configuration, the tracer's X-Ray segment structure, the metrics namespace and dimensions. The Lambda layers inventory must list each layer and its purpose, and note whether the layer contains logic that is specific to Lambda's execution model or generic logic that could be distributed as a container build dependency. The exit cost estimate section must document, for each function group, the estimated engineering effort to migrate to Step Functions orchestration, to ECS Fargate, and to Cloud Run (if multi-cloud migration is within scope). The exit trigger section must document the three measurable conditions that initiate a migration evaluation: the cost trigger (Lambda monthly cost exceeds N× the Fargate equivalent at current invocation volume), the capability trigger (a production timeout or cold start incident on a function that cannot be remediated within the cold start policy's parameters), and the strategic trigger (organizational or compliance condition that requires migration). The exit cost estimate and exit trigger together convert the vendor lock-in question from "we'll deal with it if it becomes a problem" to "we know what the problem costs to solve and under what conditions we solve it."

The fifth section documents the async invocation error model. The retry policy section must specify the retry behavior per trigger type: for SQS-triggered functions, the event source mapping's maximumRetryAttempts and bisectBatchOnFunctionError configuration (bisecting the batch on error reduces the scope of reprocessing when one message in a batch is the source of the failure, at the cost of increasing the number of Lambda invocations per failed batch), the visibility timeout configuration on the SQS queue relative to the Lambda function's configured timeout (the visibility timeout must be at least 6× the function's configured timeout to prevent messages from becoming visible again while the Lambda function is still processing them during retries), and the maximum receive count before messages are moved to the DLQ. For SNS-triggered functions, the retry policy is governed by SNS's delivery retry policy (15 retries with exponential backoff by default), which cannot be configured per-function. For EventBridge-triggered functions, the EventBridge rule's retry policy specifies the number of retries before the event is sent to the rule's dead-letter queue. The DLQ configuration section must specify that every async trigger that can fail must have a DLQ configured, the DLQ's retention period (at least as long as the maximum acceptable investigation window for a failed message), and the monitoring and alerting on DLQ depth (a CloudWatch alarm on the DLQ's ApproximateNumberOfMessagesVisible metric that fires when DLQ depth exceeds a threshold, routing to the team's alert channel). The idempotency requirement section must specify which handler implementations are required to be idempotent (all async handlers that produce side effects — database writes, external API calls, file system writes, email sends, payment requests — are required to be idempotent), the idempotency key derivation per trigger type, the idempotency check mechanism and its storage backend (commonly DynamoDB, with an idempotency table keyed by the function name and the idempotency key, with a TTL attribute set to the idempotency window duration), and the idempotency window duration per function class. The poison message handling section must specify the process for investigating messages that have been moved to the DLQ: who receives the DLQ depth alert, what investigation steps are required, whether DLQ messages are automatically or manually replayed after investigation, and how the replayability of DLQ messages is tested before the function is deployed to production. The queue and messaging decision record must be consistent with the async invocation error model: the queue's message retention period, visibility timeout, and maximum receive count configuration are the SQS side of the Lambda event source mapping's retry behavior, and a change to either side without updating the other can produce retry loops or message loss that neither the queue configuration nor the Lambda configuration reveals in isolation.

The infrastructure platform selection session, the background processing architecture session, and the cost optimization session each produce serverless compute decisions whose long-term operational cost — a four-to-eight-second cold start on a synchronous user-facing endpoint discovered through an SLA breach investigation eighteen months after the function's package crossed a cold start threshold that was never defined; or a three-week emergency Step Functions migration triggered by a production timeout on an enterprise customer's first large report, when the growth-bounded workload classification and migration cost estimate should have been written at the time the report generation function was deployed — exceeds what a cold start budget, a workload duration classification policy, and a migration cost pre-computation would have cost at the time the founding decisions were made. The decisions are in the AI chat history: the FaaS platform selection rationale, the trigger integration choices, the first async architecture that introduced at-least-once delivery without an idempotency policy, the cost optimization session that right-sized one function's memory without producing a fleet-wide cost model. WhyChose's open-source extractor surfaces these founding infrastructure sessions as structured decision records before the next cold start SLA breach, the next execution duration timeout incident, or the next DLQ depth alert on a queue with no documented poison message handling process reveals the undocumented operational commitments as an unplanned engineering sprint or an enterprise customer support escalation. The new CTO onboarding problem in the context of a Lambda-based architecture is a cold start audit and workload classification problem: an incoming technical leader will find a function fleet with package sizes that were never measured against a cold start budget, growth-bounded workloads with no duration proximity monitoring, a vendor coupling inventory that was never written, and no document explaining under what conditions the Lambda compute model would be re-evaluated — or what that re-evaluation would cost.

Further reading