Definition
- AWS Lambda – a serverless compute service that runs code only on invocation, scales automatically from zero to thousands of concurrent executions, and bills per millisecond.
- Cold start – the added latency when Lambda initializes a new execution environment (downloading code, starting the runtime, running init code).
- Maximum execution time – fifteen minutes; functions that exceed this must be containers or be broken into Step Functions steps.
- Memory-CPU coupling – you configure memory (128 MB to 10 GB) and CPU scales proportionally; you cannot set CPU independently.
- Statelessness – each invocation may run in a different environment with no reliable local state; external stores are required.
- Provisioned concurrency – pre-warms a specified number of environments to eliminate cold starts for latency-sensitive paths.
Think of two ways to staff a desk at National Bank of India. A container is a clerk who sits at the desk all day, ready instantly -- great if customers come constantly, but you pay the clerk even when no one shows up. Lambda is an on-call clerk who only walks in when a specific form arrives, handles it, and leaves -- you pay only for those minutes, but there is a short wait while the clerk walks in (the cold start), the clerk must finish within a strict time limit, cannot keep personal notes between visits (statelessness), and to give the clerk a faster brain you also have to give them a bigger desk (memory and CPU are tied).
Understanding the Concepts
When should you choose Lambda?
Lambda shines for event-driven and intermittent work. When an S3 upload, an SQS message, an API Gateway request, or an EventBridge event occurs, Lambda runs your function, scales automatically with the number of events, and you pay only for the milliseconds it runs. When nothing happens, it costs nothing.
// Lambda handler for S3 event: index a newly uploaded statement PDF
package com.bank.lambda;
import java.util.Map;
public class StatementIndexerHandler {
// Initialized once on cold start, reused on warm invocations
private static final IndexingService indexer = new IndexingService();
public String handleRequest(Map<String, Object> s3Event, Object context) {
String bucket = extractBucket(s3Event);
String key = extractKey(s3Event);
indexer.indexStatement(bucket, key);
return "Indexed: " + key;
}
private String extractBucket(Map<String, Object> event) { return "nbi-statements"; }
private String extractKey(Map<String, Object> event) { return "2026/10/SAV2001.pdf"; }
}
class IndexingService {
void indexStatement(String bucket, String key) {
System.out.println("Indexing statement: " + bucket + "/" + key);
}
}
When should you keep a container?
Keep a container on ECS or EKS when the workload is long-running, steady, latency-sensitive in a way cold starts would harm, needs more than Lambda's limits, or requires full runtime control. If a function runs flat out around the clock, always-on containers are cheaper than per-millisecond Lambda billing.
// Decision matrix for Lambda vs Container
package com.bank.infrastructure;
public class ComputeDecision {
public static String recommend(int durationMinutes, boolean eventDriven, boolean steadyLoad) {
if (durationMinutes > 15) return "Container (exceeds Lambda 15-min limit)";
if (steadyLoad) return "Container (cheaper when always-on)";
if (eventDriven) return "Lambda (scales to zero, pay per use)";
return "Lambda (default for short, intermittent work)";
}
}
What is a cold start and how do you mitigate it?
When Lambda needs a fresh execution environment -- the first invocation or when scaling out -- it downloads your code, starts the runtime, and runs initialization. This adds latency from tens of milliseconds to a few seconds. For Java, JVM startup makes this worse. Mitigation strategies include initializing expensive clients outside the handler (so warm invocations skip init), using provisioned concurrency to pre-warm environments, using SnapStart for Java, and keeping deployment packages small.
// Initialize expensive clients ONCE outside the handler
package com.bank.lambda;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import java.util.Map;
public class FeeCalculatorHandler {
// Created on cold start, reused on all warm invocations
private static final DynamoDbClient dynamo = DynamoDbClient.create();
public String handleRequest(Map<String, Object> event, Object context) {
// Fast on warm starts: dynamo client already initialized
return dynamo.describeEndpoints().toString();
}
}
Cold start timeline: [Download code] → [Start JVM/runtime] → [Run static init] → [Handle request] |___________ cold start latency __________________________| |__ fast ____| Warm invocation: [Handle request] ← no init needed, environment reused |__ fast ________|
What is the fifteen-minute execution limit?
Lambda caps execution at fifteen minutes (900 seconds). A function that exceeds this is terminated. Long batch jobs must be run as containers or broken into steps using Step Functions, where each step is a short Lambda and the state machine manages the flow.
// Breaking a long job into Step Functions steps
package com.bank.lambda;
public class LongJobSplitter {
public static void main(String[] args) {
System.out.println("NBI nightly reconciliation: ~40 minutes total");
System.out.println(" Cannot run as a single Lambda (15-min limit)");
System.out.println(" Solution 1: ECS container task (no time limit)");
System.out.println(" Solution 2: Step Functions with 3 Lambda steps:");
System.out.println(" Step 1: Extract transactions (10 min)");
System.out.println(" Step 2: Match and reconcile (10 min)");
System.out.println(" Step 3: Generate report (5 min)");
}
}
How does memory-CPU coupling work?
You configure memory from 128 MB to 10240 MB, and CPU scales proportionally. At 1769 MB you get one full vCPU. You cannot set CPU independently. A CPU-bound function sometimes needs more memory than it uses just to get more CPU, which affects cost.
// Memory-CPU coupling impact
package com.bank.lambda;
public class MemoryCpuCoupling {
public static void main(String[] args) {
System.out.println("Memory -> CPU allocation:");
System.out.println(" 128 MB -> ~0.07 vCPU (slow)");
System.out.println(" 512 MB -> ~0.29 vCPU");
System.out.println(" 1769 MB -> 1.0 vCPU (full core)");
System.out.println(" 3538 MB -> 2.0 vCPU");
System.out.println(" 10240 MB -> ~6.0 vCPU");
System.out.println();
System.out.println("CPU-bound function may need extra memory just for CPU.");
}
}
Why is Lambda stateless and what does that mean?
Each invocation may run in a different execution environment. You cannot rely on local disk or in-memory state surviving between invocations. Any state must go to an external store like DynamoDB, S3, or ElastiCache. The small /tmp space (up to 10 GB) is ephemeral and only sometimes reused on warm environments.
// Stateless Lambda: state goes to DynamoDB
package com.bank.lambda;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import java.util.Map;
public class StatelessProcessingHandler {
private static final DynamoDbClient dynamo = DynamoDbClient.create();
public String handleRequest(Map<String, Object> event, Object context) {
// Store processing state in DynamoDB, not in memory
dynamo.putItem(PutItemRequest.builder()
.tableName("nbi-processing-state")
.item(Map.of(
"txnId", AttributeValue.fromS(event.get("txnId").toString()),
"status", AttributeValue.fromS("PROCESSED")))
.build());
return "State saved to DynamoDB, not in Lambda memory";
}
}
What are additional Lambda limits to be aware of?
Beyond the core constraints, Lambda has limits on payload size (6 MB synchronous, 256 KB async), deployment package size (50 MB zipped, 250 MB unzipped, or 10 GB with container images), concurrent executions per account (default 1000, raisable), and /tmp storage (10 GB). Very chatty synchronous chains of Lambdas can be both slow and costly.
// Lambda limits summary
package com.bank.lambda;
public class LambdaLimits {
public static void main(String[] args) {
System.out.println("Execution time: 15 minutes max");
System.out.println("Memory: 128 MB to 10240 MB");
System.out.println("Sync payload: 6 MB");
System.out.println("Async payload: 256 KB");
System.out.println("Package (zip): 50 MB");
System.out.println("Package (unzipped):250 MB");
System.out.println("Container image: 10 GB");
System.out.println("/tmp storage: 10 GB");
System.out.println("Concurrency: 1000 default (raisable)");
}
}
What about provisioned concurrency and SnapStart?
Provisioned concurrency pre-warms a specified number of execution environments so they are always ready, eliminating cold starts for latency-sensitive paths like the payment API. SnapStart (for Java) takes a snapshot of the initialized JVM and restores it on invocation, dramatically reducing Java cold start times.
// Provisioned concurrency: pre-warmed environments
package com.bank.lambda;
public class ProvisionedConcurrencyExplainer {
public static void main(String[] args) {
System.out.println("Without provisioned concurrency:");
System.out.println(" First request: cold start (2-5 seconds for Java)");
System.out.println();
System.out.println("With provisioned concurrency = 10:");
System.out.println(" 10 environments are always warm and ready");
System.out.println(" First 10 concurrent requests: no cold start");
System.out.println(" 11th concurrent request: cold start (scales out)");
System.out.println();
System.out.println("SnapStart (Java): JVM snapshot restored on start");
System.out.println(" Cold start: ~200ms instead of 2-5 seconds");
}
}
Visual Overview
Lambda vs Container Decision
┌─────────────────────────────────────────────┐
│ What is the workload? │
└───────────────────┬─────────────────────────┘
│
┌─────────────┴──────────────┐
▼ ▼
Short, event-driven, Long-running,
spiky, intermittent steady, heavy
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Lambda │ │ Container │
│ │ │ (ECS/EKS) │
│ Scales to │ │ │
│ zero │ │ Always-on │
│ Pay/ms │ │ No time limit│
│ No servers│ │ Full control │
└──────────┘ └──────────────┘
Cold Start vs Warm Invocation
Cold Start:
┌──────────────┬────────────────┬────────────┬─────────────┐
│ Download code│ Start runtime │ Static init│ Handle req │
│ │ (JVM for Java) │ │ │
└──────────────┴────────────────┴────────────┴─────────────┘
|<---------- cold start latency ----------->|<-- fast --->|
Warm Invocation:
┌─────────────┐
│ Handle req │ Environment reused, no init
└─────────────┘
|<-- fast --->|
Key Takeaway
I choose AWS Lambda over a container when the workload is genuinely event-driven, short-lived, and spiky or infrequent, because Lambda runs only when an event arrives, scales automatically from zero to thousands of concurrent executions, and bills per millisecond, costing nothing when idle. I keep the workload on a container in ECS or EKS when it is long-running, steady, latency-sensitive in a way cold starts would hurt, or exceeds Lambda's limits.
The four constraints I always weigh are cold starts, the fifteen-minute limit, memory-CPU coupling, and statelessness. Cold starts add initialization latency when Lambda spins up a fresh environment, which is worse for Java; I mitigate with init outside the handler, provisioned concurrency, and SnapStart. The fifteen-minute limit means anything longer needs a container or Step Functions. Memory and CPU are coupled, so a CPU-bound function sometimes gets extra memory purely for CPU. And Lambda is stateless: each invocation may run in a different environment, so state goes to DynamoDB, S3, or a cache.
Beyond the core constraints, there are limits on payload size, package size, and concurrency that shape what fits on Lambda versus what belongs on a container.
At National Bank of India I ran the statement-PDF indexer as a Lambda because it fires a few hundred times a day in bursts and costs nothing between runs. I kept the transfer API on ECS Fargate because it serves steady traffic all day and cannot afford cold-start latency. The nightly reconciliation job, which runs about forty minutes, could not be a single Lambda because of the fifteen-minute limit, so I ran it as an ECS task orchestrated by Step Functions.
Remember one sentence: choose Lambda for short, event-driven, spiky work that scales to zero, and keep containers for long-running, steady, or heavy workloads that exceed Lambda's fifteen-minute limit or need predictable latency.
---