Sitemap
Engineering Under Pressure

We write about building systems that survive the real world. Backend architecture, AI engineering, DevOps, cloud, system design, performance, debugging, and production failures no theory for theory’s sake, just engineering lessons earned the hard way.

AWS Lambda vs ECS: Our Serverless Bill Made the CTO Question Everything

We moved our backend to AWS Lambda to stop paying for idle servers. Four months later, our most expensive service was the one that barely did any useful work.

16 min read5 hours ago

--

The argument started with a spreadsheet our CTO had prepared over the weekend. It showed how much we were spending on ECS services that continued running overnight, when most of our customers were asleep and traffic dropped considerably. He highlighted the idle capacity in red, added a column labeled potential serverless savings, and announced that we were paying Amazon to keep computers warm for absolutely no reason.

Our infrastructure engineer agreed that some services were overprovisioned. Our Staff Engineer suggested reviewing resource utilization and scaling policies before changing the deployment model. The CTO had already decided that AWS Lambda was the answer, which made the meeting feel less like an architecture review and more like a product demonstration where the product was somebody else’s opinion.

The migration looked sensible on paper. Lambda charged primarily for requests and execution duration, while our ECS deployment maintained running tasks regardless of whether they were handling traffic. We had twelve backend services, several scheduled jobs, and an event-processing pipeline that appeared well suited to serverless execution.

Four months later, the finance team wanted to know why the AWS bill hadn’t fallen as promised. Some workloads had become cheaper, but others were more expensive, and our busiest API had developed latency problems that weren’t present in the original container deployment. We had also introduced a surprising amount of infrastructure around functions that were supposed to eliminate infrastructure management.

The uncomfortable discovery was that our CTO had compared idle ECS capacity with Lambda’s execution pricing while ignoring the actual shape of our workloads. He had optimized for the easiest number to put in a spreadsheet. Unfortunately, AWS bills for considerably more than the number executives remember from a conference presentation.

The Backend Before We Discovered Serverless

Our application handled customer accounts, billing, subscriptions, document processing, and internal analytics. Most services were written in Java and Spring Boot, with PostgreSQL providing transactional storage and SQS handling asynchronous work. We ran the application on ECS using Fargate, which removed the need to manage EC2 hosts directly while still giving us container-based deployments.

The architecture wasn’t particularly exotic. HTTP services ran continuously, background workers consumed messages, and scheduled jobs handled periodic maintenance. We used CloudWatch for operational monitoring and configured scaling policies for selected services.

The main weakness was resource utilization. Several services had been assigned more CPU and memory than they typically required, and some maintained multiple tasks even during quiet periods. Our scaling configuration was conservative because the team preferred paying for spare capacity over explaining why customers couldn’t log in during a traffic spike.

Those decisions deserved investigation. But the existing infrastructure also provided predictable application startup, reusable database connections, and stable performance for long-running requests. Our Spring Boot services could initialize once and handle many requests over their lifetime.

The CTO saw those continuously running tasks as waste. He wasn’t entirely wrong, but he treated all idle capacity as unnecessary expense without considering what that capacity provided.

We approved a phased migration to Lambda. The first services were simple, the early results looked promising, and the team became increasingly confident that serverless would solve our infrastructure cost problem.

Then we migrated the account API.

Week 1: Lambda Made ECS Look Expensive

The first workload we moved was a scheduled reporting job. It ran periodically, performed a bounded amount of work, and spent most of the day doing absolutely nothing. Maintaining a continuously running worker for that task had been difficult to justify.

Lambda was a natural fit. We packaged the job, configured its trigger, and allowed AWS to invoke it when required. The function ran, completed its work, and stopped consuming execution resources until the next invocation.

The deployment was straightforward once we’d adapted the packaging and configuration. We no longer needed a continuously running ECS task for that particular job, and the billing model aligned more closely with the workload. The infrastructure team considered it a success.

Marcus, our cloud engineer, presented the result during the weekly review. He explained that Lambda was ideal because we weren’t paying for idle compute between invocations. The CTO immediately asked why the remaining services hadn’t been migrated.

I pointed out that the reporting job executed occasionally, while the account API handled requests throughout the day. Marcus replied that Lambda could scale automatically, which was true but didn’t answer whether it would be cheaper or faster for that workload.

The distinction mattered because Lambda pricing depends on execution characteristics. Request volume, memory configuration, execution duration, architecture, provisioned concurrency, and other features influence the bill. ECS costs depend on task resources, running time, and the infrastructure surrounding the deployment.

Comparing the two without considering workload shape is like comparing taxi fares with car ownership while refusing to mention how often you drive.

The CTO didn’t particularly enjoy that analogy.

The Spring Boot Cold Start Nobody Budgeted For

The account API was one of our busiest services. It handled authentication-related workflows, account details, and subscription information through several HTTP endpoints. The existing ECS deployment maintained warm application instances with initialized Spring contexts and established connection pools.

Moving the service to Lambda changed its execution lifecycle. A new Lambda execution environment needed to initialize the runtime and application before it could handle requests. For a Java application with substantial framework initialization, that startup work could be noticeable.

We measured cold and warm invocations separately. Warm requests behaved differently from requests that required a new execution environment, and the distribution depended on traffic patterns and concurrency. Average latency alone wasn’t enough to explain the user experience because occasional initialization delays could affect tail latency.

Our first mistake was treating cold starts as a fixed penalty. They weren’t. Initialization behavior depended on the runtime, deployment artifact, configuration, dependency initialization, and whether an invocation reused an existing execution environment.

We experimented with reducing unnecessary initialization work and reviewing application dependencies. We also evaluated Lambda features and deployment strategies that could improve startup behavior for supported runtimes and configurations. Those approaches helped us understand the trade-offs, but they didn’t make the underlying lifecycle identical to a continuously running ECS service.

Marcus argued that cold starts were becoming less important as AWS improved the platform. I agreed that the available mitigations had evolved, then asked whether we had measured their effect on our application and included any additional charges in the cost model.

He said we should focus on the architecture rather than obsess over every dollar.

The finance manager would later disagree rather strongly.

AWS Lambda vs ECS Performance: The Benchmark That Changed the Mood

We created a representative performance test covering low traffic, sustained traffic, and sudden concurrency increases. Both implementations used the same business logic where practical, and we measured end-to-end response time rather than only function execution duration. The test also tracked downstream database activity and resource utilization.

At low request volumes, Lambda’s billing model was attractive because execution resources weren’t continuously allocated in the same way as our ECS tasks. For intermittent workloads, that difference could be significant. The application still needed to account for API integration, logging, networking, and other supporting costs.

At sustained traffic levels, the comparison became less straightforward. Lambda accumulated execution charges across invocations, while ECS spread its running task costs across the requests those tasks served. A well-utilized ECS service could become economically attractive because its compute resources were already available.

The latency comparison also depended on application behavior. ECS maintained long-running application processes, which helped workloads that benefited from initialized frameworks and reusable resources. Lambda could perform well with appropriately designed handlers, but new execution environments introduced lifecycle considerations we didn’t have in the same form with ECS.

We refused to publish a universal performance winner. The results varied with request duration, concurrency, memory configuration, and the amount of time the service spent waiting for downstream dependencies.

Our CTO wanted to know whether Lambda was faster. I explained that we’d identified workloads where Lambda was attractive and others where ECS remained competitive or preferable. He asked why architecture decisions always became complicated after engineers started measuring things.

Nobody had a particularly diplomatic answer.

The Database Connection Problem That Arrived With Concurrency

The next problem appeared during a traffic spike. Lambda could create additional execution environments to handle concurrent requests, which sounded excellent until we examined how those environments interacted with PostgreSQL. Each environment could maintain its own database connections, and the aggregate connection count became a concern as concurrency increased.

Our ECS services had relatively predictable task counts and connection pool configurations. We could estimate the maximum number of database connections based on the number of running tasks and each application’s pool size. The Lambda deployment required us to reconsider those assumptions because execution environments could scale independently.

We tested concurrency increases while monitoring PostgreSQL connections, query latency, and application errors. The database became a limiting factor before the theoretical scaling capacity of Lambda was useful. Adding more concurrent application execution didn’t increase database capacity.

We evaluated connection-management strategies, including reducing unnecessary connections and using appropriate managed connection pooling options such as RDS Proxy where applicable. We also reviewed whether every invocation needed direct database access or whether some workflows could be decoupled through asynchronous processing.

The important point was that Lambda’s scaling behavior didn’t eliminate downstream bottlenecks. It could increase the number of callers reaching those bottlenecks, sometimes very quickly.

Our architect described the situation as a database capacity issue. Daniel, our database engineer, replied that the database had been perfectly comfortable before the application started inviting hundreds of new connections to the same party.

The discussion was less entertaining when the connection errors appeared in the production dashboard.

The Hidden Cost of Waiting

One of our APIs spent a substantial portion of its execution time waiting for external services. The handler performed some business logic, called a third-party API, waited for a response, and then wrote the result to PostgreSQL. The CPU work was modest, but the invocation remained active while waiting.

That distinction mattered for Lambda billing. Execution duration includes time spent waiting during the invocation, so an application can accumulate billed duration even when it isn’t performing much CPU work. Memory configuration also influences the compute charge, making the relationship between performance and cost more complicated than request count alone.

ECS tasks also consume resources while waiting, but their cost model is based on provisioned task resources and running time rather than charging separately for each request’s duration. A continuously running service can handle overlapping I/O-bound requests, potentially spreading its resource cost across substantial throughput.

We measured how much time our handlers spent performing useful computation versus waiting for dependencies. Several functions had long execution durations because downstream calls were slow or unpredictable. Those functions weren’t automatically poor Lambda candidates, but their economics differed from the short, intermittent jobs that had motivated the migration.

We also investigated timeouts and retries. A slow dependency could cause repeated invocations, and poorly controlled retries could amplify both downstream load and execution charges. The same application reliability mistakes that caused incidents in containers could become direct billing problems under a per-execution model.

Marcus suggested increasing memory because additional CPU allocation could reduce execution time for some functions. That was worth testing, but it wasn’t guaranteed to help a handler dominated by external network latency. Paying for more compute doesn’t make a third-party API answer faster simply because the request came from a more expensive execution environment.

The finance manager eventually asked why our serverless architecture was charging us while the application waited for another company’s servers.

It was a reasonable question, although the answer wasn’t particularly satisfying.

The AWS Bill Was More Than Lambda Duration

Our initial cost estimate focused heavily on Lambda requests and compute duration. The actual deployment involved additional services and infrastructure, including API integration, networking, logging, monitoring, and data transfer. Some costs were present in both architectures, while others changed because of our deployment decisions.

API Gateway pricing mattered for HTTP workloads using that integration. CloudWatch logs and metrics also contributed to operating costs, particularly when functions generated verbose output at high invocation volumes. Network architecture could introduce additional charges depending on how functions accessed resources and external services.

We reviewed our use of VPC networking and associated infrastructure. Certain configurations can involve NAT gateways and data processing charges, while other architectures avoid those costs. The important lesson was that networking expenses couldn’t be inferred from Lambda pricing alone.

The ECS comparison required the same discipline. We included Fargate task resources, load balancing, logging, networking, and the cost of maintaining capacity for availability requirements. We also considered engineering time because operating a service includes deployment procedures, monitoring, incident response, and troubleshooting.

Our original spreadsheet had compared one narrow component of each architecture. It made Lambda look cheaper because it ignored several costs that appeared only after we implemented the complete system.

Get Architecture Unfiltered’s stories in your inbox

Join Medium for free to get updates from this writer.

The CTO asked whether the finance team had included every relevant charge. The finance manager said she’d included the charges AWS had actually billed us, which was an unusually effective way to end an architecture debate.

The Cost Equation We Should Have Used

We eventually built a model that reflected the actual workload. For Lambda, the compute portion depended on invocation count, allocated memory, billed duration, and the applicable pricing dimensions. Request charges and any additional features or integrations needed to be considered separately.

For ECS on Fargate, the compute portion depended on allocated vCPU and memory, task running time, and applicable configuration or pricing options. A service maintaining multiple tasks continuously would have a different cost profile from a workload that could scale down during quiet periods.

The model also needed traffic distribution. A service receiving occasional bursts might favor Lambda, while a consistently busy service could favor ECS when its tasks were efficiently utilized. The break-even point wasn’t universal because it depended on the application’s execution characteristics and deployment configuration.

We included operational constraints as well. Lambda has execution-duration limits and concurrency considerations, while ECS supports long-running processes and more flexible application lifecycles. Those differences can determine the architecture before pricing becomes the deciding factor.

The model didn’t produce a single recommendation for all twelve services. That frustrated the CTO because the migration proposal had promised one standardized deployment strategy. It was also the first cost analysis that accurately reflected the diversity of our workloads.

We should have built it before approving the migration.

Instead, we built it after paying for the lesson.

The Workloads Where Lambda Actually Won

Despite the problems, Lambda was the right choice for several parts of our platform. Scheduled jobs with short execution times were strong candidates because they didn’t require continuously running compute. Event-driven tasks with irregular traffic also benefited from the ability to process work when events arrived.

Some lightweight APIs performed well on Lambda, particularly when their initialization overhead was controlled and their execution patterns aligned with the platform. For these workloads, the operational convenience and consumption-based pricing could justify the design.

We also valued the integration with AWS event sources. Lambda could respond to events from services such as SQS and EventBridge, reducing the amount of infrastructure needed for certain asynchronous workflows. That convenience was meaningful when the functions were designed with appropriate retry and failure-handling behavior.

The critical requirement was understanding the workload. Short, intermittent execution with manageable concurrency and predictable dependencies often made Lambda attractive. Long-running, consistently busy, or connection-heavy services required more careful evaluation.

We kept Lambda where it provided measurable benefits. The mistake had been assuming that those benefits applied equally to every service simply because they were all called backend applications.

The Workloads Where ECS Made More Sense

Our busiest HTTP services were better suited to continuously running containers under the conditions we measured. They handled sustained traffic, benefited from initialized application state, and maintained reusable connections to downstream systems. ECS allowed us to manage task resources and scaling policies around those characteristics.

Long-running background processes also remained natural container workloads. Some workers needed to maintain connections, coordinate processing, or execute tasks that didn’t fit comfortably within Lambda’s execution model. ECS provided more control over application lifecycle and resource allocation.

We improved the existing ECS deployment rather than returning to the original configuration unchanged. Resource requests were revised based on measured utilization, scaling policies were adjusted, and unnecessary idle capacity was reduced where availability requirements allowed it.

Those changes addressed part of the waste that had motivated the migration. They also demonstrated that the original ECS bill wasn’t a fixed property of container deployment. It reflected choices we’d made about capacity, scaling, and operational safety.

Our infrastructure engineer admitted that we’d treated serverless as a shortcut around capacity planning. The platform could automate certain scaling responsibilities, but it couldn’t decide which workloads were economically appropriate or how much concurrency the database could safely handle.

That still required engineers.

Unfortunately for our CTO, those engineers continued asking questions.

AWS Lambda vs ECS: Which Is Better for Microservices?

The answer depends on the service’s workload and operational requirements. Lambda can be attractive for event-driven microservices, intermittent traffic, and bounded execution where consumption-based pricing aligns with actual usage. ECS can be attractive for continuously running services, sustained traffic, and applications requiring greater control over process lifecycle.

Microservices architecture doesn’t require every service to use the same compute platform. A system can run event handlers on Lambda while hosting long-running APIs and workers on ECS. The important part is maintaining clear service boundaries and operational consistency where it matters.

Latency requirements also influence the decision. Applications sensitive to startup delays need to evaluate Lambda initialization behavior and available mitigation strategies. ECS services can maintain warm processes, although they still require careful scaling and deployment configuration.

Cost comparisons should include complete infrastructure charges and realistic traffic patterns. A low-traffic service and a consistently busy API may reach different conclusions even when they use the same programming language and database.

We eventually stopped discussing whether the company should be serverless. We started discussing which execution model best matched each service.

That was a much more useful architecture conversation.

Frequently Asked Questions About AWS Lambda vs ECS

Is AWS Lambda cheaper than ECS?

Lambda can be cheaper for intermittent workloads because compute charges are tied to execution rather than continuously running tasks. ECS can be more economical for sustained workloads with efficiently utilized resources. The comparison depends on execution duration, memory, concurrency, traffic patterns, and supporting infrastructure.

Is AWS Lambda faster than ECS?

Neither platform is universally faster. Lambda performance depends on execution environment initialization, runtime behavior, resource configuration, and application design. ECS can provide predictable performance for continuously running applications, while Lambda can perform well for appropriately designed functions.

Does AWS Lambda have cold starts?

Yes. New Lambda execution environments may require initialization before handling requests, although the impact depends on runtime, configuration, traffic, and available optimization features. Cold starts should be measured using the application’s actual deployment rather than assumed from generic benchmarks.

Can AWS Lambda replace ECS?

Lambda can replace ECS for some workloads, particularly event-driven functions and short-running tasks. Other applications may require long-running processes, more flexible execution lifecycles, or resource management better suited to containers. A complete replacement isn’t always practical or economical.

Is ECS serverless?

Amazon ECS is a container orchestration service, and ECS with AWS Fargate allows teams to run containers without managing the underlying servers. The term serverless is sometimes applied to Fargate’s infrastructure model, although its pricing and execution behavior differ from Lambda.

Is Lambda good for Java and Spring Boot?

Lambda can run Java applications, including applications using Spring technologies, but startup behavior and deployment design deserve careful attention. Framework initialization and dependency configuration can influence cold starts. Teams should benchmark the specific runtime and application configuration they intend to deploy.

Is Lambda better for low traffic?

Lambda is often attractive for low or irregular traffic because execution resources don’t need to remain continuously allocated in the same way as ECS tasks. However, latency requirements, networking, and supporting service costs can influence the decision. Low traffic alone doesn’t guarantee the lowest total cost.

Does Lambda scale automatically?

Lambda can scale execution environments in response to incoming workload, subject to concurrency quotas and scaling behavior. However, downstream systems such as databases may not scale at the same rate. Applications should control concurrency and protect dependencies from overload.

What is the maximum execution time for AWS Lambda?

AWS Lambda has a maximum invocation duration of 15 minutes. Workloads that require longer continuous execution may need a different architecture, such as ECS tasks or an orchestration approach that divides work into smaller units.

Is ECS better for long-running applications?

ECS is often a good fit for long-running services and workers because containers can maintain application processes over extended periods. It provides control over task resources, deployment behavior, and scaling. The decision should still account for operational requirements and cost.

Why is my AWS Lambda bill so high?

High Lambda costs can result from frequent invocations, long execution durations, memory allocation, retries, provisioned concurrency, and related infrastructure. Logs, API integrations, networking, and data transfer can also contribute. Cost analysis should identify which components and workloads are responsible.

Can Lambda and ECS be used together?

Yes. Many architectures use Lambda for event-driven processing and ECS for continuously running services or workers. The combination can be effective when each platform is assigned workloads that match its strengths. Teams should still manage observability, security, and operational standards consistently.

Four Months Later, We Finally Stopped Calling Everything Serverless

We didn’t abandon Lambda after the cost review. Several functions remained cheaper and simpler to operate than their previous container-based equivalents, and the event-driven workloads benefited from the platform’s integration with AWS services. Those successes were real, even if they didn’t justify migrating the entire backend.

We moved selected high-traffic services back to ECS and improved their resource utilization. The application retained predictable startup behavior, reused connections more effectively, and avoided some execution costs that had accumulated under sustained traffic. We also corrected the database connection and retry issues uncovered during the migration.

The CTO eventually accepted that the original cost model had been incomplete. He still believed serverless was an important part of the company’s architecture, and I agreed. The difference was that we now evaluated Lambda and ECS based on workload characteristics rather than treating one deployment model as a universal improvement.

Our finance manager was less interested in the philosophical discussion. She wanted to see costs explained by service, traffic pattern, and business value. That requirement forced us to measure the economics of the architecture instead of celebrating the number of functions we’d deployed.

The most useful outcome was a better engineering process. Every proposed migration now needed a baseline, a representative performance test, a complete cost model, and a clear rollback strategy. We also required teams to identify downstream scaling constraints before assuming that additional application concurrency would improve throughput.

We had started the project because we didn’t want to pay for idle servers. Four months later, we discovered that paying for every invocation wasn’t automatically cheaper, especially when the application spent much of its time waiting for dependencies.

AWS Lambda remained a powerful tool. ECS remained a strong choice for workloads that benefited from continuously running containers. The expensive mistake was believing that one billing model could tell us which architecture was correct.

The next time our CTO arrived with a spreadsheet showing enormous potential savings, we asked him to include the workload assumptions before approving the migration.

He started bringing smaller spreadsheets.

Devrim Özçay — ProdRescue by Devrim

I write about AWS architecture, backend performance, distributed systems, and the production decisions that become expensive after deployment. You can read more on my Substack, explore the free Software Engineer High-Stakes Playbook, or use the Senior & Staff System Design Interview OS to practice making architecture decisions under real constraints.

--

--

Architecture Unfiltered
Architecture Unfiltered

Brutal truths about software engineering.

Engineering Under Pressure
Engineering Under Pressure

We write about building systems that survive the real world. Backend architecture, AI engineering, DevOps, cloud, system design, performance, debugging, and production failures no theory for theory’s sake, just engineering lessons earned the hard way.