Architecture analysis
Serverless vs ECS Fargate Microservices on AWS — Architecture, Scaling and Trade-offs
The same task-management API, built twice: once on Lambda and DynamoDB, once as containerised microservices on ECS Fargate with RDS PostgreSQL. This is a walk through what each design actually does with a request, how it behaves under load, where it fails, and what you are signing up for when you choose it.
Contents
- One application, two architectures
- Architecture 1 — Serverless
- Tracing
POST /tasks - When Serverless makes sense
- Architecture 2 — ECS Fargate microservices
- Path-based routing
- From code to running task
- When ECS Fargate makes sense
- Side-by-side comparison
- The Architect's Decision
- What happens when traffic grows?
- How the cost models differ
- What I learned
One application, two architectures
The application is deliberately boring: a task manager. Users create tasks, list them, update them and delete them. A Vue.js single-page application in the browser talks to a REST API with five endpoints — POST /tasks, GET /tasks, GET /tasks/{id}, PUT /tasks/{id} and DELETE /tasks/{id}.
Boring is useful here. When the business logic is trivial, the only interesting thing left is the architecture — and it turns out there are very different ways to run the same five endpoints on AWS. One design has no servers, no network to manage and no idle compute. The other has a VPC, a load balancer, containers in two Availability Zones and a relational database with a standby. Both work. Both are reasonable. They are reasonable under different circumstances.
So the goal of this article is not to declare a winner. It is to understand what each architecture is optimised for, what it costs you in complexity, money and constraints, and how to recognise which set of trade-offs fits a given workload.
I have avoided quoting AWS prices and service quotas. Both change, both vary by Region and account, and a figure copied into a blog post is usually wrong within a year. Where I use numbers, they are labelled assumptions used to show how something scales — not claims about what AWS will charge or allow. For real figures, use the AWS Pricing Calculator and the Service Quotas console for your account.
Architecture 1Serverless: API Gateway, Lambda and DynamoDB
Request and data flow
Reading the diagram left to right, the request path is:
There are really two flows in that picture, and it is worth separating them. The first is a static content flow: the browser asks CloudFront for the application, CloudFront serves HTML, JavaScript and CSS from its edge cache or fetches them from S3. That happens once per visit (and is mostly cached). The second is the API flow: once the Vue.js application is running in the browser, it makes HTTPS calls to API Gateway every time the user does something.
The arrow from S3 to API Gateway is shorthand. S3 never calls the API — S3 only stores files. The code that S3 delivered is what calls the API, from the user's device. This distinction matters when you reason about latency, security (the API is public and must authenticate callers itself) and CORS (if the API lives on a different hostname from the site, the browser will send preflight OPTIONS requests).
The response path runs backwards along the dashed arrows:
What each component is responsible for
- Amazon CloudFrontTerminates HTTPS close to the user, caches the static site at edge locations and shields the S3 bucket from direct access. With Origin Access Control the bucket can stay private — only the distribution can read it.
- Amazon S3Durable storage for the built Vue.js assets. It does not run anything; it serves files to CloudFront.
- Amazon API GatewayThe public front door for the API. It handles TLS, routes method + path to an integration, can validate requests and authorise callers, and applies throttling before anything reaches your code.
- AWS LambdaRuns the CRUD handler code on demand. Each concurrent request gets its own execution environment; AWS creates and retires those environments as traffic changes.
- Amazon DynamoDBStores tasks in a key-value / document table. There is no connection pool and no instance to size — every request is an authenticated HTTPS call, and capacity is either on-demand or provisioned.
- AWS IAMThe Lambda execution role defines exactly what the function may do — for example
dynamodb:PutItemon one table, and writing to its own log group. IAM replaces database credentials entirely. - Amazon CloudWatchReceives function logs and the metrics that matter for serverless: invocations, errors, duration, throttles and concurrency, plus API Gateway latency and 4xx/5xx counts.
Tracing POST /tasks from browser to database
Abstract diagrams hide the interesting details, so here is what actually happens when a user clicks “Add task”.
- The SPA builds the request. The Vue.js application is already loaded from CloudFront. It sends
POST /taskswith a JSON body such as{"title":"Renew TLS cert"}and an authorisation token. If the API is on a different origin, the browser first sends a CORS preflightOPTIONSrequest and only proceeds if API Gateway returns the right headers. - API Gateway accepts or rejects it. TLS is terminated, the route
POST /tasksis matched, and — in a production version — an authoriser (Cognito, JWT or a Lambda authoriser) validates the caller. Request validation can reject a malformed body here, before you pay for a Lambda invocation. Throttling limits are applied at this point too. - Lambda is invoked. With a proxy integration, API Gateway passes the whole HTTP request to Lambda as an event. If a warm execution environment is free, it is reused. If not, Lambda creates one — downloading the code, starting the runtime and running initialisation code. That is the cold start.
- The handler runs. It parses and validates the body, generates an ID and timestamp, and writes the item to DynamoDB using credentials from the execution role. No secrets are stored in the function.
- DynamoDB persists the item. The partition key determines which partition stores the item. The write is replicated across multiple Availability Zones before success is returned.
- The response travels back. Lambda returns a status code and body, API Gateway adds headers (including CORS) and sends the response to the browser, and the SPA adds the new task to the list. In parallel, Lambda's logs and metrics flow to CloudWatch.
A minimal handler for that route looks like this (Node.js, AWS SDK v3):
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "node:crypto";
// Created once per execution environment and reused while it stays warm.
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler = async (event) => {
const input = JSON.parse(event.body ?? "{}");
if (!input.title) {
return {
statusCode: 400,
body: JSON.stringify({ message: "title is required" }),
};
}
const task = {
// A client-supplied ID lets a retried request be detected.
id: input.clientRequestId ?? randomUUID(),
title: input.title,
status: "OPEN",
createdAt: new Date().toISOString(),
};
await ddb.send(new PutCommand({
TableName: process.env.TABLE_NAME,
Item: task,
// Never overwrite an existing task silently.
ConditionExpression: "attribute_not_exists(id)",
}));
return { statusCode: 201, body: JSON.stringify(task) };
};
Two small details carry real architectural weight. The SDK client lives outside the handler so warm invocations reuse it — that is one of the cheapest ways to reduce per-request latency. And the conditional write plus a client-supplied request ID makes the endpoint safe to retry: if the browser times out and resends the same POST, the second write fails the condition instead of creating a duplicate task. Retries happen at every layer in a distributed system, so POST needs an idempotency story even in a toy application.
Scaling, concurrency and the pay-per-use model
Automatic scaling. Nothing in Figure 1 has a “number of instances” setting. API Gateway, Lambda and DynamoDB (in on-demand mode) all scale as part of the service. That does not mean scaling is infinite or instantaneous — it means the scaling mechanism is AWS's problem, and your job shifts to understanding the limits around it.
Lambda concurrency. An execution environment handles one request at a time, so the number you care about is concurrency, not requests per second. A useful approximation is:
If a request takes 100 ms, 50 requests per second need roughly five environments. If a slow downstream dependency pushes duration to one second, the same traffic needs fifty. Concurrency is drawn from a Regional pool shared by every function in the account, and new environments are added at a bounded rate. Two controls matter: reserved concurrency guarantees (and caps) capacity for one function, and provisioned concurrency keeps initialised environments ready — at the price of paying for them while idle.
DynamoDB scaling. On-demand mode adjusts to traffic without capacity planning; provisioned mode with auto scaling is often cheaper for steady, predictable load. Either way, throughput is ultimately distributed across partitions, so the key design matters more than the capacity mode. A partition key such as userId spreads load well; a key such as status (with only a few values) concentrates writes on a few partitions and throttles long before the table as a whole is busy.
Pay-per-use. You are billed for API requests, Lambda invocations and compute time (duration × configured memory), DynamoDB reads, writes and storage, and logs. With no traffic, Lambda compute costs nothing. That is the defining economic property of this design — and the reason it can be very cheap for spiky or low-volume workloads and less obviously cheap for sustained high volume.
Operational simplicity. There is no operating system to patch, no cluster, no load balancer, no subnet design, no database instance to size or fail over. The operational surface is application code, IAM policies, configuration and observability. For a small team, that is a large amount of work that simply does not exist.
Cold starts, execution limits and quotas
Cold starts. The first request to a new execution environment pays for initialisation. The impact depends on runtime, package size, VPC attachment and what your init code does — a lean Node.js or Python function feels very different from a large JVM application loading a framework. Mitigations include keeping deployment packages small, doing expensive set-up once outside the handler, SnapStart on supported runtimes, and provisioned concurrency for latency-critical paths. For a CRUD API behind a human user, occasional cold starts are usually acceptable; for a strict latency SLO, they need to be designed for.
Execution duration. A Lambda invocation can run for at most 15 minutes, and behind a synchronous API the practical limit is far lower: an API Gateway REST integration times out after 29 seconds by default. Anything long-running — report generation, large imports, video processing — has to become asynchronous: accept the request, put work on a queue or start a Step Functions workflow, and let the client poll or be notified.
Service quotas. Every managed service has quotas: account-level Lambda concurrency, API Gateway throttling limits, payload size limits, DynamoDB per-partition throughput. Many are adjustable, some are not. A serverless design that has not been checked against its quotas has an unknown failure threshold.
Observability and failure points
Observability starts in CloudWatch: structured JSON logs from the function, Lambda metrics (Errors, Throttles, Duration, ConcurrentExecutions), API Gateway latency and 4xx/5xx counts, and DynamoDB throttled request metrics. Because one user action crosses several managed services, distributed tracing (AWS X-Ray or OpenTelemetry) pays for itself quickly — it shows whether time was spent in API Gateway, in initialisation, in your code or in DynamoDB.
The failure points are specific and predictable:
- Throttling — API Gateway rejects requests over its limits; Lambda rejects invocations when concurrency is exhausted; DynamoDB throttles hot partitions. The client should retry with exponential backoff and jitter.
- Timeouts — a slow dependency hits the function timeout or the API integration timeout. The user sees an error even though the write may have succeeded, which is exactly why idempotency matters.
- Permissions — a missing IAM action fails only on the code path that needs it, often in production first. Least privilege needs tests.
- Cascading concurrency — if Lambda calls a fragile downstream system (a third-party API, a relational database), Lambda's ability to scale quickly can overwhelm it. Reserved concurrency is also a way to protect what sits behind the function.
When Serverless makes sense
- Variable or unpredictable traffic. Load that is near zero overnight and spikes during the day, or that you simply cannot forecast yet. You do not pay for idle compute, and you do not have to guess capacity.
- Event-driven systems. Lambda's natural shape is “something happened, react to it” — S3 uploads, queue messages, EventBridge events, DynamoDB streams. A REST API is one event source among many.
- Small and medium APIs. A handful of resources, CRUD-heavy logic, short request durations. The entire backend fits in a few functions and a table.
- Rapid product development. When the priority is learning whether a product works, minimising infrastructure decisions buys speed.
- Minimising infrastructure management. Teams without platform engineers, or organisations that would rather not own patching, scaling and capacity planning.
Lambda becomes less attractive when requests are long-running or need persistent connections, when sustained high throughput makes per-invocation pricing add up, when latency SLOs cannot tolerate cold starts without paying for provisioned concurrency, when the application needs heavyweight frameworks or OS-level control, or when it must talk to a relational database with a limited connection budget — each concurrent Lambda environment wants its own connection unless something like RDS Proxy pools them.
Architecture 2ECS Fargate microservices with RDS PostgreSQL
Request and data flow
The front end is identical to Architecture 1 — CloudFront and S3 deliver the same SPA. Everything changes after API Gateway. Instead of invoking a function, API Gateway forwards the request privately through a VPC Link into the VPC, where an internal Application Load Balancer chooses a service based on the URL path and sends the request to one of that service's running containers. The container queries PostgreSQL over a pooled connection, and the response returns along the same connections in reverse.
Take GET /users/42: API Gateway matches the route and hands it to the VPC Link; the ALB's listener rule for /users/* selects the User Service target group; the ALB picks a healthy task — by its private IP, because Fargate tasks each have their own network interface — and the User Service runs SELECT … FROM users WHERE id = $1 against RDS. Nothing in that path is reachable from the internet except API Gateway.
What each component is responsible for
- API GatewayStill the public entry point: TLS, authentication, throttling, request validation and a stable public contract. Keeping it in front of the ALB means the load balancer never has to be internet-facing.
- VPC LinkA private connection from API Gateway into the VPC. The diagram uses VPC Link v2, which can target an internal ALB directly; older REST API designs needed a Network Load Balancer in front of the ALB to achieve the same thing.
- Application Load BalancerLayer 7 routing across Availability Zones. It health-checks every task, removes unhealthy ones from rotation, drains connections during deployments, and routes by path to the right service.
- Amazon ECSThe orchestrator. It decides where tasks run, keeps the desired number running, replaces failed ones and coordinates rolling deployments.
- AWS FargateThe compute engine underneath ECS. You specify CPU and memory per task; AWS provides and patches the hosts. There are no EC2 instances to manage.
- ECS servicesA long-running definition such as “keep two copies of the Task Service running, registered with this target group, spread across these subnets.” Each microservice is its own ECS service.
- Fargate tasksThe running instances of a service — one or more containers, launched from a versioned task definition, each with its own private IP in a private subnet.
- Amazon ECRThe private registry for the container images. Tasks pull images from ECR when they start; images can be scanned for vulnerabilities on push.
- RDS PostgreSQLA managed relational database with a Multi-AZ standby in a second Availability Zone. If the primary fails, RDS fails over to the standby and the endpoint moves with it.
- VPCThe network boundary: private application subnets and private database subnets in two Availability Zones. As the diagram notes, private tasks still need a route to ECR and CloudWatch — via NAT gateways or VPC endpoints.
- Security groupsChained trust. The ECS security group accepts traffic only from the ALB's security group on the application port; the RDS security group accepts only TCP 5432 from the ECS security group.
- Service Auto ScalingAdjusts each service's desired task count from CloudWatch metrics such as CPU, memory or ALB requests per target.
- IAMTwo roles per service. The task execution role lets ECS pull the image and write logs. The task role is what the application code itself is allowed to do in AWS.
- CloudWatchContainer logs, ECS service CPU and memory, ALB request count, latency, target health and 5xx errors, and RDS metrics such as connections, CPU and replica lag.
Path-based routing on the ALB
One internal ALB fronts all three services. Its listener evaluates rules in priority order and forwards each request to a target group:
Each rule matches both the bare path and the wildcard — the pattern the diagram uses — rather than /tasks*, which would also match /tasks-archive and quietly send it to the wrong service. A fixed 404 as the default action means an unknown path fails loudly at the edge of the VPC instead of landing on whichever service happens to be the default. This routing layer is also what makes the services independently deployable: the ALB only knows about target groups, so a new version of the User Service can roll out without the other two noticing.
From code to running task
The deployable unit in this architecture is a container image, and it moves through a fixed sequence:
A few practices make that pipeline trustworthy: tag images with the commit SHA and make tags immutable so a deployed version can never change underneath you; keep images small, because image size directly affects how long a new task takes to start; and enable the ECS deployment circuit breaker so a version whose tasks keep failing health checks is rolled back automatically.
ECS Service Auto Scaling
Each service scales independently by changing its desired count. A typical configuration for the Task Service:
When the metric stays above target, Application Auto Scaling raises the desired count; ECS launches new tasks across both AZs, they pull their image, start, pass health checks and register with the ALB. That takes on the order of tens of seconds to a few minutes, not milliseconds — which is why the minimum is set to cover a sudden rise in traffic, and why target values sit below saturation. Scale-in is deliberately slower, and the ALB's deregistration delay lets in-flight requests finish before a task is stopped.
Design considerations
Long-running workloads. A container runs until you stop it. Long requests, WebSockets, streaming responses, background workers and in-memory caches are all natural here in a way they are not inside a 15-minute, one-request-at-a-time function.
Container control. You choose the base image, OS packages, language runtime and version, and process model. Any framework that runs in Docker runs here. The price is responsibility: base images need patching and rebuilding, and image vulnerabilities are your problem even though the host is not.
Independent deployment and scaling. Each service has its own image, task definition, target group and scaling policy. The Task Service can deploy twice a day and scale to ten tasks while the Notification Service runs two tasks and deploys monthly.
Networking. This is the biggest jump in complexity: VPC design, subnets per AZ, route tables, security groups, NAT or VPC endpoints for ECR, CloudWatch Logs and Secrets Manager, and the VPC Link. None of it is hard individually; all of it has to be correct at once, and mistakes tend to present as timeouts rather than clear errors.
Service boundaries. Three services are only better than one if the boundaries are real — if Tasks, Users and Notifications change for different reasons, at different rates, possibly owned by different people. Split along the wrong lines and you get a distributed monolith: every feature needs coordinated deployments, plus network calls that used to be function calls.
Database ownership. The diagram is candid that this is a learning architecture with a shared database. One PostgreSQL instance is simpler to run and allows joins and transactions across services — but it also couples them through the schema. A migration for the User Service can break the Task Service, and a heavy query from one service affects all of them. A stricter design gives each service its own schema or database, and moves cross-service consistency into APIs or events. That is more robust and considerably more work.
Operational complexity. There are now more things to own: container images and their vulnerabilities, task sizing, scaling policies, health-check tuning, database engine upgrades, parameter groups, backups, connection management and VPC configuration. Fargate removes host management, not service management.
Minimum running infrastructure. At zero traffic, this architecture still runs at least two tasks per service across two AZs (six tasks in total), an ALB, a Multi-AZ RDS instance, and NAT gateways or interface endpoints. That baseline is what buys availability and instant response — and it is a cost that exists whether anyone uses the application or not.
Observability and failure handling
Observability spans more layers. The ALB reports request count, latency percentiles, 5xx errors and healthy-host count per target group; ECS reports service CPU and memory (with Container Insights for task-level detail); RDS reports connections, CPU, storage and replication health; and application logs flow from each container to CloudWatch Logs. Correlating a single slow request across API Gateway, ALB, service and database needs a request ID propagated through headers and, ideally, distributed tracing.
Failure handling is a mix of what the platform does for you and what you have to design:
- Task failure — a crashed container or failed health check is removed from the target group and replaced by the ECS scheduler.
- Bad deployment — the deployment circuit breaker stops a rollout whose tasks never become healthy and rolls back to the last working version.
- Availability Zone impairment — tasks in the other AZ keep serving while the service rebalances, provided the minimum count left enough headroom in one AZ.
- Database failover — RDS promotes the standby, but existing connections are dropped. Applications must reconnect and retry; a pooler such as RDS Proxy can shorten the disruption.
- Connection exhaustion — scaling out tasks multiplies database connections. This is the most common way a container architecture “scales” straight into a database outage.
- Image pull failures — if private subnets lose their route to ECR (NAT or endpoint misconfiguration), no new task can start, which typically surfaces during a deployment or a scale-out — exactly when you need it.
When ECS Fargate makes sense
- Long-running APIs. Requests that take longer than a synchronous API timeout, connections that stay open, or processes that need warm in-memory state.
- Containerised applications. Existing services already packaged as Docker images, or frameworks built around a long-lived process.
- Microservices. Several services with genuinely separate lifecycles, owned by different teams, that benefit from independent deployment and scaling.
- Predictable, sustained workloads. Steady traffic keeps running tasks well utilised, where per-request pricing can become the more expensive model.
- Greater runtime control. Specific OS packages, language versions, sidecars, custom agents or tuned process settings.
- Container-native teams. Teams already fluent in Docker, image pipelines and container debugging lose little and gain portability.
Side-by-side comparison
Summarised, the two designs differ on almost every axis — and in each row, “lower” or “higher” is a trade, not a score.
| Area | Serverless | ECS Fargate Microservices |
|---|---|---|
| Compute | AWS Lambda functions | Containers running as Fargate tasks |
| Scaling | Lambda concurrency, managed by AWS | ECS Service Auto Scaling of task count |
| Database | DynamoDB (key-value / document) | RDS PostgreSQL (relational, Multi-AZ) |
| Runtime control | Lower — managed runtimes, limits on duration and payload | Higher — your image, OS packages and process model |
| Infrastructure management | Lower — no network, cluster or database instance | Higher — VPC, ALB, cluster, database sizing |
| Deployment unit | Function | Container image |
| Networking complexity | Lower — managed service endpoints | Higher — subnets, security groups, NAT/endpoints, VPC Link |
| Idle compute cost | Very low — potentially none for Lambda compute | Running tasks have ongoing cost |
| Startup behaviour | Cold starts possible | Tasks normally remain running |
| Workload style | Event / request driven | Long-running services |
| Microservice isolation | Possible — per-function roles and deployments | Natural container and service boundary |
| Operational complexity | Lower initially | Higher |
| Portability | More AWS / serverless-specific | Container workloads are generally more portable |
The Architect's Decision
It is tempting to read Figure 2 as the “more advanced” architecture because it has more boxes. That is the wrong lens. Every additional component is something to secure, monitor, patch, pay for and understand at 3 a.m. Architecture is the practice of choosing which problems you want to have. Below, each quality attribute is examined as a move: what changes when you go from Lambda + DynamoDB to ECS Fargate + RDS.
Cost
The move trades variable cost for fixed cost. That is a bad deal for an internal tool used a few hours a week, and can be a good deal for a service with high, steady traffic where running tasks are well utilised.
Example: a staging environment of Figure 2 costs nearly as much as production overnight, because the floor is the same. In Figure 1, an unused staging environment costs close to nothing.
Scalability
Serverless scaling is fast and implicit; container scaling is slower and explicit. Explicit is not worse — it is predictable and testable — but someone has to choose the metrics, the targets and the limits.
Example: a marketing email drives a sudden spike. Lambda absorbs it as concurrency rises; the Fargate service relies on spare capacity in its running tasks while new ones take time to start.
Performance
The move removes cold starts and adds query flexibility, but adds network hops (VPC Link, ALB) and introduces database contention as a new performance variable.
Example: “tasks due this week, grouped by assignee, with user names” is one SQL query in PostgreSQL. In DynamoDB it needs a deliberately designed index — or a different data model — because access patterns must be known in advance.
Availability
The move turns availability from a property you receive into a property you design and pay for. Figure 2 does it correctly — but each element is a decision that could have been made differently, for example to save money.
Example: setting the minimum task count to one to cut cost quietly removes AZ resilience from that service.
Reliability
Neither is inherently more reliable. Reliability comes from handling the failure modes each one actually has.
Example: during an RDS failover, existing connections drop. A service without reconnect-and-retry logic serves errors until it is restarted; one with a resilient pool recovers on its own.
Security
The move adds network isolation as a layer of control, and also adds image vulnerabilities and credentials as new risks. Both architectures depend on least-privilege IAM — one function role versus a task role per service.
Example: a vulnerable library in a base image is your patching problem on Fargate. On Lambda, the managed runtime is patched by AWS, though your own dependencies are still yours.
Operational complexity
The move multiplies the number of things that can be misconfigured. That is acceptable when a team has the skills and the system needs the control — and expensive when neither is true.
Example: a PostgreSQL major-version upgrade is a planned change with testing and a maintenance window. DynamoDB has no equivalent event.
Developer experience
The move usually feels more familiar to developers used to conventional web frameworks, and less familiar to those who learnt cloud development event-first.
Example: reproducing a bug in the Task Service can be docker compose up with a database seed. Reproducing a Lambda/DynamoDB bug often means deploying to a sandbox account or emulating services.
Deployment complexity
The move adds a build artefact and a pipeline stage, and — with a shared database — adds the hardest deployment problem in the system: schema changes that must stay compatible with every service version currently running.
Example: renaming a column used by two services needs an expand-and-contract migration over several releases, not one deployment.
Observability
The move increases the number of dashboards and the importance of a consistent request ID across layers.
Example: a latency spike in Figure 2 could be the ALB, a saturated task, a noisy neighbour service, or the shared database. Without per-layer metrics and tracing, that is guesswork.
Maintainability
Maintainability is mostly about boundaries, not technology. Both architectures can be maintainable; both can become a tangle.
Example: grouping related routes into one Lambda per resource (rather than one per endpoint) often keeps a serverless codebase easier to maintain.
Vendor coupling
The move reduces coupling at the application and data layers, not at the platform layer. Whether that matters depends on whether leaving AWS is a realistic requirement or a theoretical one — and portability that is never exercised has a real cost.
Example: moving the Task Service to Kubernetes means rewriting deployment manifests, not application code. Moving the Lambda version means reworking handlers and the data access layer.
What happens when traffic grows?
A thought experiment: take each architecture through four orders of magnitude of traffic, and at each step ask which component needs attention first. The rates are illustrative, and the arithmetic uses stated assumptions — the point is to see where the bottleneck moves, not to predict exact capacity.
For the serverless side, I will assume an average Lambda duration of 100 ms, so concurrency ≈ requests per second × 0.1. For the container side, the number of tasks depends on how many requests one task can sustain, which only a load test can tell you — so I describe it as a variable rather than inventing a number.
-
10 req/s≈ 1 concurrent Lambda
Serverless
- Nothing needs attention. A handful of warm environments, DynamoDB barely noticing.
- Cold starts are proportionally more visible, because environments idle out between bursts.
ECS Fargate
- The minimum task count (two per service) is mostly idle.
- Cost is dominated by the baseline, not by load. Right-sizing task CPU and memory is the main lever.
-
100 req/s≈ 10 concurrent Lambdas
Serverless
- Still comfortable. Watch DynamoDB key distribution and whether on-demand or provisioned capacity is the better cost fit.
- Log volume starts to show up in the bill.
ECS Fargate
- Auto scaling becomes real — tune the target metric and cooldowns.
- Database connections become worth counting: tasks × pool size per task.
-
1,000 req/s≈ 100 concurrent Lambdas
Serverless
- Check account concurrency and API Gateway throttling quotas against peak, not average.
- Hot partitions surface if the key design is uneven; throttling appears on specific keys first.
- Downstream dependencies — third-party APIs, anything with connection limits — now feel Lambda's parallelism.
ECS Fargate
- More tasks per service; scale-out speed matters, so headroom in the minimum count matters.
- The ALB scales automatically, but very sharp spikes deserve a load test.
- RDS becomes the focal point: CPU, IOPS, and connections. Connection pooling (in-app pools, RDS Proxy) is no longer optional.
-
10,000 req/s≈ 1,000 concurrent Lambdas
Serverless
- Quota increases are planned work, requested well ahead of launch.
- Lambda's scaling rate matters for sudden spikes; provisioned concurrency may be justified.
- Per-request pricing at sustained volume is worth modelling against alternatives.
- DynamoDB pre-warming (warm throughput) for known events; caching hot reads.
ECS Fargate
- The compute tier still scales horizontally; the single PostgreSQL primary does not.
- Read replicas, caching (ElastiCache), query optimisation, and eventually splitting the shared database per service.
- Writes on one primary become the architectural limit.
The pattern is the important part. In the serverless design, the bottleneck starts nowhere and moves outward — to quotas, to partition design, and finally to whatever non-serverless system the functions depend on. In the container design, the compute tier scales comfortably and the bottleneck moves down, into the database: first connection count, then CPU and I/O, finally the write throughput of a single primary. In both cases, the component that limits growth is rarely the compute layer people spend the most time debating.
Connections are where container architectures most often get surprised. With 10 tasks per service, 3 services and a pool of 10 connections per task, the database sees up to 300 connections. Scale each service to 40 tasks and it is 1,200 — against a max_connections limit set by the instance's memory. Scaling the compute tier can push the database past its limit, which is why the maximum task count is as much a database-protection setting as a cost setting.
How the cost models differ
The two architectures do not just cost different amounts — they are priced along different dimensions, which is why “which is cheaper?” has no general answer.
Serverless
Container architecture
The serverless formula has almost no fixed term: if nobody uses the application, the compute cost is close to zero, and every unit of cost corresponds to a unit of work. The container formula is dominated by fixed terms: tasks run (and are billed for their CPU and memory) whether they are busy or idle, the ALB and RDS instance run continuously, and NAT gateways or interface endpoints have hourly charges in each AZ.
This is why serverless is attractive for intermittent workloads: internal tools, early-stage products, APIs with strong daily cycles, event processors that sit idle between bursts. You never pay for capacity waiting for traffic.
And it is why continuously running containers can make more sense for sustained workloads: if tasks are busy most of the time, you pay for CPU and memory that you are actually using, and the cost per request falls as utilisation rises. Commitment discounts such as Savings Plans and interruption-tolerant capacity such as Fargate Spot further change the picture for steady load.
Three cost items are easy to miss in either design. CloudWatch Logs ingestion grows with traffic and verbosity and can rival compute cost for chatty services. Networking — NAT gateway data processing and cross-AZ traffic — is a real line item in Figure 2 that does not appear on the diagram at all. And provisioned concurrency reintroduces an idle cost into the serverless model, quietly turning it into something closer to the container curve.
The only honest way to compare them for a real system is to write down the assumptions — requests per month, average duration and memory, item sizes, peak-to-average ratio, required availability — and run both through the AWS Pricing Calculator. The output is less important than the assumptions it forces you to state.
What I learned from comparing these architectures
Building the same application twice made a few things clear that are easy to miss when you only ever build one.
The compute choice drags the data choice with it. Lambda pairs naturally with DynamoDB because neither needs connections or capacity planning; Fargate pairs naturally with PostgreSQL because long-lived processes hold connection pools well. Many of the real differences between these architectures — query flexibility, scaling ceilings, schema coupling, failover behaviour — are database differences wearing a compute label.
Complexity is a cost, not a credential. Figure 2 has more services, more networking and more resilience features. It also has more ways to fail, more to monitor and a permanent baseline bill. Those extras are justified when the workload needs long-running processes, runtime control or independently owned services — not because the diagram looks more impressive.
Bottlenecks move; they do not disappear. Serverless moves them to quotas and downstream systems. Containers move them to the database. Knowing where the next bottleneck will be is more useful than knowing how fast the current tier can scale.
Every architecture needs the same disciplines. Idempotent writes, retries with backoff, least-privilege IAM, structured logs, meaningful alarms and a tested rollback path apply to both designs. The platform changes which failures you see, not whether you need to plan for them.
The right answer can change over time. A product might reasonably start as Figure 1 — fast to build, almost free at low traffic — and move parts of itself to Figure 2 as traffic becomes sustained, requests become longer or teams grow. Designing clean API boundaries early is what makes that move possible later.
Architecture is not about selecting the most advanced AWS service. It is about selecting the simplest architecture that satisfies the workload's scalability, reliability, security, performance, cost and operational requirements.
Diagrams and analysis by Binu. Service behaviour described here reflects AWS documentation at the time of writing; check current service quotas and pricing for your Region before making decisions based on them.