DevOps NetworkDevOpsNetwork
DevOps NetworkDevOpsNetwork

Menu

DashboardDaily ChallengePlannerLeaderboardRoadmapHubsInterview ExperiencesModulesCheatsheetsTech BlogQuizzesInterview PrepProjectsResourcesReport Bug

More

TopicsConceptsGlossaryCommunity
Join Free
DevOps NetworkDevOpsNetwork
Dashboard
Daily Challenge
Planner
Leaderboard
Roadmap
Interview Experiences
ResourcesReport Bug

AWS Interview Questions

50 real AWS interview questions with detailed answers covering EC2, S3, IAM, VPC, Lambda, DynamoDB and cost optimization — grouped by difficulty.

50questions with answers
Questions
EASY (12)
Question 1What is the difference between an AWS Region and an Availability Zone?Question 2What is Amazon S3, and what problem does it solve?Question 3What is the difference between IAM users, groups, and roles?Question 4What is an Amazon EC2 instance, and how is it different from a traditional server?Question 5What is Amazon VPC?Question 6What is the difference between Amazon EBS and instance store volumes?Question 7What is Auto Scaling in AWS?Question 8What is the difference between stopping and terminating an EC2 instance?Question 9What is Amazon CloudWatch used for?Question 10What is the difference between SQS and SNS?Question 11What are the three main AWS storage types — object, block, and file — and what's an example of each?Question 12What is Amazon CloudFront, and why put it in front of an S3 bucket instead of serving straight from S3?
MEDIUM (25)
Question 13What's the difference between a Security Group and a Network ACL?Question 14How does DynamoDB decide which partition an item lives on, and what happens when a partition gets "hot"?Question 15When would you choose DynamoDB over RDS, and vice versa?Question 16What's the difference between a NAT Gateway and a NAT instance?Question 17Walk me through how IAM decides whether to allow or deny a request when multiple policies apply.Question 18What's the difference between an Application Load Balancer and a Network Load Balancer, and when would you pick each?Question 19What are the S3 storage classes, and how would you decide between them?Question 20What happens during a cold start in AWS Lambda, and how would you reduce it?Question 21What's the difference between horizontal and vertical scaling on AWS, and which does Auto Scaling give you?Question 22How does Multi-AZ differ from a Read Replica in RDS?Question 23What's the difference between Reserved Instances, Savings Plans, and Spot Instances?Question 24What is the purpose of a VPC endpoint, and what's the difference between a gateway endpoint and an interface endpoint?Question 25How would you troubleshoot an EC2 instance that can't reach an S3 bucket in the same account?Question 26What are DynamoDB Streams, and what would you use them for?Question 27What's the difference between an EBS snapshot and an AMI?Question 28What's the difference between synchronous and asynchronous invocation in Lambda, and why does it matter for error handling?Question 29What's the difference between CloudFormation and Terraform, and why would a shop pick one over the other on AWS?Question 30What's the difference between an internet-facing Application Load Balancer and API Gateway for exposing a Lambda function?Question 31How does S3 handle consistency today, and why did that used to be an interview trap?Question 32What's the difference between a GSI and an LSI in DynamoDB?Question 33What is the shared responsibility model, and where's the line for something like RDS versus EC2?Question 34What's the difference between KMS envelope encryption and just encrypting data directly with a KMS key?Question 35What's the difference between AWS Secrets Manager and Systems Manager Parameter Store, and when would you use each?Question 36What are Route 53's main routing policies, and how would you choose between weighted, latency-based, and failover?Question 37What is Amazon ElastiCache, and what problem does it solve that a database alone doesn't?
HARD (13)
Question 38Design a disaster recovery strategy for an application that needs an RTO of 15 minutes and RPO of 1 minute. What would you pick and why?Question 39A DynamoDB table is throttling even though its aggregate consumed capacity is well under the provisioned total. What's going on, and how do you fix it?Question 40Your Lambda function reads from S3, writes to DynamoDB, and publishes to SNS. How would you scope its IAM role, and what's wrong with just attaching PowerUserAccess?Question 41You're seeing intermittent 5xx errors from an ALB-fronted Auto Scaling group under load, but CPU on the instances looks fine. How would you debug it?Question 42When would you choose ECS over EKS, or vice versa, for a team that's never run containers on AWS before?Question 43How would you design a multi-region active-active architecture on top of DynamoDB, and what do you give up to get it?Question 44An engineer accidentally committed AWS access keys to a public GitHub repo. Walk through what you'd do in the first hour.Question 45VPC Peering or Transit Gateway: how would you decide, and what changes as the number of VPCs grows?Question 46Your team wants to cut EC2 spend by 40% without hurting availability. What's your approach?Question 47Explain the trade-offs between Pilot Light, Warm Standby, and Multi-Site Active/Active DR strategies.Question 48Why might a serverless architecture end up more expensive than an EC2-based one at scale, and how would you decide?Question 49Your application's database queries are fast in testing but slow under real production load, and the database's CPU is low. Where would you look?Question 50How would you design tracing and observability for a request that flows through API Gateway, two Lambda functions, and DynamoDB, so you can tell where time is being spent?

A Region is a physical location in the world — like eu-west-1 in Ireland — that contains multiple, isolated data centers. An Availability Zone (AZ) is one of those data centers (or a cluster of them) inside a Region, with its own power, cooling, and networking.

The reason this distinction matters is fault isolation. AZs within a Region are connected by low-latency private links, so you can run a highly available application by spreading it across two or three AZs in the same Region — if one AZ has a power or network failure, the others keep serving traffic. Going cross-Region is a separate, heavier decision, usually reserved for disaster recovery or serving users on different continents with lower latency.

The common wrong answer here is treating "Region" and "AZ" as interchangeable, or assuming that deploying to multiple Regions is what "high availability" means by default — in practice, multi-AZ is the first rung, and multi-Region is a much bigger jump in cost and complexity.

  • AWS Global Infrastructure

Amazon S3 (Simple Storage Service) is object storage: you store and retrieve files ("objects") inside buckets, addressed by a key, over HTTPS, without managing any servers or disks yourself.

It solves the problem of durable, virtually unlimited storage that doesn't need capacity planning. Instead of provisioning a disk and worrying about running out of space, you just write objects to a bucket and S3 handles replication across multiple Availability Zones behind the scenes, giving it 99.999999999% ("eleven nines") durability for objects stored in the Standard class. It's commonly used for static website hosting, backups, data lakes, and as the origin for a CDN like CloudFront.

S3 is not a filesystem and not a database — it has no native way to update part of an object or run a query across objects, which is the detail that trips people up when they try to use it like a shared drive with heavy read/write contention.

  • Amazon S3 documentation

A user is a permanent identity for a specific person or application, with its own long-term credentials. A group is just a named collection of users that lets you attach the same permissions to all of them at once. A role is an identity with no permanent credentials at all — something (a person, an EC2 instance, a Lambda function, another AWS account) assumes it temporarily and gets short-lived credentials for the duration.

The practical reason roles matter is that they remove the need to store or rotate long-term secrets. An EC2 instance with a role attached can call S3 or DynamoDB without any access key sitting on disk; a Lambda function does the same via its execution role.

The common wrong answer is describing roles as "a type of user." A role is closer to a costume than an identity of its own — it grants a set of permissions to whoever puts it on, and nobody is permanently "logged in" as a role.

  • IAM identities documentation

An EC2 instance is a virtual machine that runs in AWS's data centers, launched from a template called an AMI (Amazon Machine Image) and billed by the second or hour it runs.

The main differences from a physical server you'd rack yourself are elasticity and lack of hardware ownership. You can launch an instance in minutes, resize it to a bigger or smaller instance type without buying new hardware, and turn it off when you don't need it so you stop paying for compute (though attached storage keeps billing separately). AWS manages everything below the hypervisor — physical hosts, racking, power — while you manage the OS and everything above it.

A definition question like this doesn't need a diagram; the useful follow-up is usually about instance types and pricing models rather than the definition itself.

  • Amazon EC2 documentation

A VPC (Virtual Private Cloud) is a logically isolated, private network inside AWS where you launch resources like EC2 instances and RDS databases. You define its IP address range, split it into subnets, and control what traffic can get in or out.

It exists because AWS is multi-tenant infrastructure — a VPC is what gives your account its own private network space, separate from every other customer's, even though the underlying hardware is shared. Every AWS account gets a default VPC in each Region to get started quickly, but production workloads almost always use a custom VPC with subnets split by purpose (public-facing vs. private) and by Availability Zone.

  • Amazon VPC documentation

EBS (Elastic Block Store) is network-attached, persistent block storage — it survives an instance stop, and you can detach it and reattach it to a different instance. Instance store is physical disk space on the host machine the instance is running on; it's faster because there's no network hop, but its data is lost the moment the instance stops or the underlying hardware fails.

That trade-off is the whole answer in practice: EBS is the default for anything you can't afford to lose, like a database volume or a boot disk, while instance store fits ephemeral, high-throughput use cases like a local cache or scratch space for a batch job, where losing the data on restart is fine.

  • Amazon EBS documentation

Auto Scaling automatically adds or removes EC2 instances in a group based on demand, using rules you define — for example, keep average CPU near 50%, or scale out when the queue backlog grows.

It matters because it turns "how many servers do I need" from a manual, static decision into something that tracks real traffic: you avoid paying for capacity you don't need overnight, and you avoid getting paged because a traffic spike outran a fixed fleet size. An Auto Scaling group also replaces unhealthy instances automatically when combined with health checks from a load balancer, which is as much a reliability feature as a cost one.

  • Amazon EC2 Auto Scaling documentation

Stopping an instance shuts it down but keeps it and its attached EBS volumes intact — you can start it again later and everything on the boot disk is still there. Terminating deletes the instance permanently, and by default deletes its root EBS volume with it (non-root volumes can be set to persist).

The distinction matters for cost and for data safety: a stopped instance stops billing for compute but you still pay for the EBS storage, while a terminated instance is gone and, unless you took a snapshot or set the volume not to delete on termination, so is its data. Instances backed by instance store can't be stopped at all — only terminated — because there's no persistent disk to preserve.

  • Instance lifecycle documentation

CloudWatch is AWS's monitoring and observability service: it collects metrics, logs, and events from AWS resources and your own applications, and lets you set alarms that trigger actions when something crosses a threshold.

In practice it's the thing standing between "the app is slow" and knowing why — CPU and memory metrics from EC2, request counts and latency from a load balancer, error rates from Lambda, custom application metrics you publish yourself, all in one place. A CloudWatch Alarm can page someone, trigger an Auto Scaling action, or invoke a Lambda function automatically, which is what turns monitoring into actual automated response rather than a dashboard someone has to watch.

  • Amazon CloudWatch documentation

SQS (Simple Queue Service) is a message queue: a producer sends a message, it sits in the queue, and one consumer pulls and processes it. SNS (Simple Notification Service) is a publish/subscribe topic: a producer publishes a message once, and it's pushed out to every subscriber at the same time — which can include multiple SQS queues, Lambda functions, email addresses, or HTTP endpoints.

The way to keep it straight is "one queue, one processor" versus "one message, many recipients." A very common real pattern is fan-out: SNS publishes an event once, and several SQS queues subscribed to that topic each get their own copy to process independently, so one slow consumer doesn't block another.

  • Amazon SQS documentation · Amazon SNS documentation

Object storage holds whole files as objects with metadata, accessed over HTTP by key — Amazon S3 is the example. Block storage exposes raw, fixed-size blocks that an OS formats and mounts like a disk — Amazon EBS is the example, and it can only be attached to one instance at a time (with some exceptions like Multi-Attach). File storage presents a shared, POSIX-style filesystem that multiple instances can mount and read/write to at once — Amazon EFS is the example.

The difference that actually decides which one you reach for is access pattern: S3 for large volumes of files accessed over the network with no need for a filesystem, EBS for a database or boot volume that needs low-latency block access from exactly one instance, and EFS for anything where several instances genuinely need to share the same live filesystem, like a content management system's upload directory.

  • Storage overview — AWS

CloudFront is AWS's content delivery network (CDN) — it caches content at edge locations around the world, so a user's request is served from a nearby edge instead of traveling all the way back to the bucket's Region.

Putting it in front of S3 buys three things a plain bucket doesn't give you on its own: lower latency for users far from the bucket's Region, since the response comes from a nearby edge cache after the first request; lower load and cost on the bucket itself, since cached objects are served from the edge without hitting S3 again; and a proper way to serve a private bucket to the public, using Origin Access Control so the bucket itself stays fully private and only CloudFront can read from it, while everyone else goes through CloudFront's URL.

The trap is skipping OAC and instead making the bucket public just to serve a website — that works, but it removes the one clean way to keep the origin locked down while still serving content globally, and it throws away the caching benefit that's the actual reason to add a CDN in the first place.

  • Amazon CloudFront documentation · Restricting access to an S3 origin

A Security Group is a stateful firewall attached to individual resources like EC2 instances — if you allow inbound traffic on a port, the matching outbound response is automatically allowed, and you can only write "allow" rules. A Network ACL (NACL) is a stateless firewall attached at the subnet level — it evaluates inbound and outbound traffic independently, rules are processed in numbered order, and it supports explicit "deny" rules.

The reason both exist is layered defense: Security Groups control what an individual instance can talk to, while NACLs control what's allowed to cross a subnet boundary at all, regardless of instance-level rules. Because NACLs are stateless, a common mistake is opening an inbound port on a NACL and forgetting the matching outbound rule for the response traffic — the request gets in, but the reply never leaves.

Security Group Network ACL
Level Instance/ENI Subnet
State Stateful Stateless
Rules Allow only Allow and deny
Evaluation All rules In rule-number order
  • VPC security documentation

DynamoDB hashes an item's partition key to decide which physical partition stores it, and each partition has a fixed throughput ceiling — roughly 3,000 read capacity units and 1,000 write capacity units. A "hot" partition happens when far more traffic lands on one partition key's value than others, so that partition hits its ceiling and starts throttling requests even though the table as a whole has plenty of unused capacity.

This is why partition key choice is a design decision, not an implementation detail: a key like status with only a handful of possible values concentrates almost all writes onto a few partitions, while a high-cardinality key like a UUID or a composite of user ID and timestamp spreads load evenly. DynamoDB's adaptive capacity helps absorb short-lived imbalances automatically, but it isn't a substitute for a key that distributes traffic well in the first place. The tell-tale symptom in production is ProvisionedThroughputExceededException on some requests while CloudWatch shows the table's aggregate consumed capacity is nowhere near its limit.

  • Choosing a partition key documentation

Choose DynamoDB when you have known, simple access patterns at very high scale and want single-digit-millisecond latency without managing servers — it's a managed key-value/document store with no joins and no ad-hoc query language. Choose RDS when you need relational integrity: foreign keys, multi-table transactions, complex joins, or ad-hoc queries you can't fully predict up front.

The trap is treating this as "NoSQL is for scale, SQL is for small apps" — that's not the real trade-off. DynamoDB scales further with less operational effort, but it demands that you design your table around your access patterns before you write any data, often duplicating data across items to avoid joins. RDS lets you query flexibly after the fact, at the cost of needing to manage instance sizing, connections, and eventually sharding if you outgrow a single writer. A lot of real systems use both: RDS for the transactional core, DynamoDB for a high-throughput piece like session state or an activity feed.

  • DynamoDB documentation · Amazon RDS documentation

Both let resources in a private subnet reach the internet (for things like package updates) without being directly reachable from it. A NAT Gateway is a managed AWS service — you create it, AWS handles scaling, patching, and availability within an AZ. A NAT instance is just a regular EC2 instance running NAT software, which you provision, patch, and scale yourself.

In practice almost nobody chooses a NAT instance for a new build today: NAT Gateway costs more per hour but removes the operational burden and scales to very high bandwidth automatically, while a NAT instance is cheaper at low, steady traffic and can be resized or turned into a bastion host, but becomes a manual scaling and patching problem as traffic grows. One detail worth knowing: a NAT Gateway lives in a single Availability Zone, so a highly available design needs one NAT Gateway per AZ, not one shared across a whole Region.

  • NAT gateway documentation

IAM starts from an implicit deny, then evaluates every applicable policy — identity-based policies on the user or role, resource-based policies like an S3 bucket policy, permissions boundaries, and Service Control Policies if the account is in an AWS Organization. If any policy in that set contains an explicit Deny that matches the request, the request is denied immediately and nothing else matters. Otherwise, if at least one policy contains an explicit Allow that matches, the request is allowed; if nothing explicitly allows it, the implicit deny stands.

The order to remember is: explicit deny beats everything, explicit allow beats implicit deny, and no match means deny. This is the piece that trips people up in interviews and in real incidents — someone adds a broad Allow policy expecting it to grant access, but an unrelated Deny statement elsewhere (often in an SCP they forgot existed) silently wins, and the request still fails with no obvious reason why in the calling application's error message.

◈ DIAGRAM
Explicit Deny ─▶ DENY (stop)
│ no match
▼
Explicit Allow ─▶ ALLOW
│ no match
▼
(default) ─▶ DENY
  • IAM policy evaluation logic documentation

An Application Load Balancer (ALB) operates at the HTTP/HTTPS layer (Layer 7) — it can route based on URL path or hostname, terminate TLS, and inspect headers. A Network Load Balancer (NLB) operates at the TCP/UDP layer (Layer 4) — it just forwards connections, with no visibility into the content, but at much higher throughput and lower, more consistent latency.

Pick an ALB for a typical web application or microservices setup where you want path-based or host-based routing to different backend services from one load balancer. Pick an NLB when you need extreme performance and low latency, need to preserve the client's source IP without extra configuration, need to support non-HTTP protocols, or need a static IP address for the load balancer itself — something an ALB doesn't offer directly. It's common to put an NLB in front of an ALB when you need both content routing and a static IP.

  • Elastic Load Balancing documentation

S3 offers several storage classes that trade retrieval speed and availability for lower storage cost: Standard for frequently accessed data, Standard-IA and One Zone-IA for infrequently accessed data you still need quickly, Intelligent-Tiering for data with unpredictable access patterns, and the Glacier family (Instant Retrieval, Flexible Retrieval, Deep Archive) for archival data you rarely touch and can wait minutes to hours to restore.

The decision comes down to two questions: how often is this object actually read, and how long can I wait to get it back if I need it? A backup you hope to never restore belongs in Glacier Deep Archive; a product image on a live website belongs in Standard; log files nobody's looked at in 90 days but that might need pulling for an audit fit Glacier Flexible Retrieval. Most real buckets use a lifecycle policy to move objects down that ladder automatically as they age, rather than picking one class up front and never revisiting it.

  • S3 storage classes documentation

A cold start is the extra latency on the first invocation of a function instance: Lambda has to provision a new execution environment, download your code, start the runtime, and run any initialization code outside your handler before it can process the event. Subsequent invocations that reuse that same warm environment skip all of that and just run the handler, which is why cold starts show up as occasional latency spikes rather than a constant tax.

To reduce it: keep the deployment package small and trim unused dependencies, since download and init time scale with package size; move expensive setup (SDK clients, DB connections) outside the handler so it only runs once per environment, not once per invocation; choose a lower-overhead runtime if latency is critical (compiled languages generally cold-start faster than ones needing a heavier runtime); and for latency-sensitive, low-traffic functions, use Provisioned Concurrency to keep a set number of environments pre-warmed. The trap is applying Provisioned Concurrency everywhere — it removes the auto-scaling cost benefit that makes Lambda attractive in the first place, so it's worth reserving for the specific functions where cold-start latency is actually a user-facing problem.

  • Lambda execution environment documentation

Vertical scaling means making one instance bigger — moving to an instance type with more CPU or memory. Horizontal scaling means adding more instances of the same size and spreading load across them. EC2 Auto Scaling gives you horizontal scaling: it launches and terminates instances in a group based on demand, rather than resizing an existing instance while it's running.

Horizontal scaling is generally preferred for availability, since it removes a single point of failure and lets you scale near-continuously with demand, but it requires the application to be stateless or to externalize its state (sessions in a shared cache, files in S3 rather than local disk) so any instance can handle any request. Vertical scaling is simpler and needs no application changes, but it has a ceiling — the largest instance type — and typically requires a restart to resize, which horizontal scaling avoids.

  • Amazon EC2 Auto Scaling documentation

Classic Multi-AZ keeps a synchronously replicated standby copy of your database in a different Availability Zone purely for failover — the standby isn't readable, and its only job is to take over automatically if the primary fails. A Read Replica is an asynchronously replicated, independently readable copy you create to offload read traffic from the primary; it can live in the same Region or a different one, and you can have several of them.

They solve different problems and are often used together: Multi-AZ answers "what happens if my primary dies," giving high availability with automatic failover typically in under a minute; Read Replicas answer "my primary is getting hammered with read queries," giving horizontal read scaling. The trap is assuming a Read Replica gives you high availability — because replication to it is asynchronous, it can lag behind the primary, and promoting one to be the new primary during an outage is a manual (or scripted) action, not an automatic failover.

One nuance worth knowing for a follow-up: AWS also offers Multi-AZ DB clusters (currently MySQL and PostgreSQL) with one writer and two standbys that are readable, giving failover and read scaling in a single deployment with failovers typically under 35 seconds — so "Multi-AZ standbys are never readable" is only true of the older, single-standby Multi-AZ instance deployment, not this newer option.

  • Amazon RDS Multi-AZ documentation

On-Demand is the baseline: pay per second with no commitment. Reserved Instances and Savings Plans both trade a 1- or 3-year commitment for a discount of up to around 70% off On-Demand — Reserved Instances commit to a specific instance family and Region, while Savings Plans commit to a dollar amount of compute spend per hour and apply automatically across instance families and even to Lambda and Fargate, which makes them more flexible. Spot Instances let you use AWS's unused capacity at up to 90% off, but AWS can reclaim them with about two minutes' notice whenever it needs that capacity back.

The decision follows the shape of the workload: steady, predictable baseline load is the case for Reserved Instances or Savings Plans, since you know you'll use that capacity for the full term regardless. Fault-tolerant, interruptible, or batchable work — CI/CD runners, big data jobs, stateless workers in an Auto Scaling group — is the case for Spot, ideally with a fallback to On-Demand so a burst of interruptions doesn't take the workload down entirely. Unpredictable or short-lived workloads stay On-Demand, since committing to something you might not run for the full term erases the savings.

  • Amazon EC2 pricing documentation

A VPC endpoint lets resources in a private subnet reach an AWS service (like S3 or DynamoDB) without the traffic leaving the AWS network over the public internet — no NAT Gateway or internet gateway required. A gateway endpoint works by adding a route in your route table and only supports S3 and DynamoDB; it's free and has no bandwidth charge. An interface endpoint creates an elastic network interface with a private IP inside your subnet, backed by AWS PrivateLink, and supports most other AWS services — but it bills per hour and per gigabyte processed.

The reason this matters beyond cost is security posture: it lets you keep a subnet with no route to the internet at all while still letting instances in it talk to S3, which is a meaningfully tighter design than opening a NAT Gateway just so instances can reach one AWS service.

  • VPC endpoints documentation

Work outward from the identity, then the policy, then the network. First check whether the instance actually has an IAM role attached, and whether that role's policy grants the specific action being called — s3:GetObject for a read is not the same permission as s3:ListBucket, and missing the second one is a very common reason a call fails even though "the role has S3 access." Next check the bucket policy itself for an explicit Deny — remember from IAM evaluation logic that one explicit deny anywhere in the chain wins over any number of allows, so a bucket policy locking access to a specific VPC endpoint or account will block a call even with a perfectly good IAM role. Then check network path: if the subnet is private with no NAT Gateway and no S3 VPC endpoint, the instance has no way to reach S3 at all, regardless of permissions.

A quick way to isolate which layer is failing is the error itself: a 403 Forbidden almost always means IAM or bucket policy, while a timeout or connection error points at networking. The trap is jumping straight to "add more permissions" — a 403 from an overly broad Deny statement won't be fixed by adding an Allow, and a networking failure won't be fixed by touching IAM at all.

  • Troubleshooting Amazon S3 documentation

DynamoDB Streams is an ordered, time-ordered log of every insert, update, and delete on a table, retained for 24 hours, that other services can subscribe to — most commonly an AWS Lambda function, which is invoked automatically as new records appear.

You'd reach for it any time something needs to react to a data change rather than poll for it: keeping a search index like OpenSearch in sync with the table, invalidating a cache entry when the underlying item changes, sending a notification when an order is placed, or replicating changes to another system. The detail worth knowing for a follow-up question is that Streams guarantees ordering only within a single partition key, not across the whole table, and delivery is at-least-once — so a consumer needs to be written to handle the same record arriving twice without causing a duplicate side effect.

  • DynamoDB Streams documentation

An EBS snapshot is a point-in-time, incremental backup of a single volume, stored in S3 behind the scenes. An AMI (Amazon Machine Image) is a template for launching an EC2 instance — it references one or more snapshots (the root volume and any attached volumes) plus metadata like the OS, architecture, and launch permissions.

The relationship is that an AMI is built on top of snapshots, not a replacement for them: you'd take a snapshot to back up or restore a single volume's data, but you'd create an AMI when you want to launch new, identical instances — for example, baking a "golden image" with your application pre-installed so an Auto Scaling group can launch new instances from it without a configuration step at boot.

  • Amazon EBS snapshots documentation

A synchronous invocation waits for the function to finish and returns its response directly to the caller — an API Gateway request calling Lambda is the classic example. An asynchronous invocation hands the event to Lambda and returns immediately; Lambda queues the event internally and processes it separately, without the original caller waiting or getting the result back — S3 event notifications and SNS triggers work this way.

This changes how you have to handle failures. With a synchronous call, the caller sees the error immediately and decides whether to retry. With an asynchronous call, Lambda itself retries automatically (twice, by default, with a delay) and, if all retries fail, the event is dropped unless you've configured a Dead Letter Queue or an on-failure destination to catch it. The trap is assuming "asynchronous" means "fire and forget, nothing to worry about" — without a DLQ or destination configured, a persistently failing async event just silently disappears after the retries are exhausted.

  • Lambda invocation types documentation

CloudFormation is AWS's own infrastructure-as-code service — you write a template (YAML or JSON) describing resources, and AWS provisions and tracks them as a "stack," natively, with no separate tool to run. Terraform is a third-party, multi-cloud tool from HashiCorp that uses its own language (HCL) and works the same way across AWS, other clouds, and many other providers, tracking state in a file it manages itself.

CloudFormation tends to win when a team is AWS-only and wants tight native integration — things like StackSets for multi-account deployment, or drift detection built in — without managing state storage themselves. Terraform tends to win when a team is multi-cloud, wants a larger third-party module ecosystem, or already has Terraform expertise elsewhere in the organization. In practice the deciding factor is usually organizational rather than technical: which tool does the team already know, and does anything outside AWS need to be provisioned by the same pipeline.

  • AWS CloudFormation documentation

Both can sit in front of a Lambda function and be reached from the internet, but they're built for different jobs. API Gateway is purpose-built for APIs: it gives you request validation, API keys and usage plans, built-in throttling per client, request/response transformation, and native integration with Lambda and Cognito for auth — all without you writing that logic yourself. An ALB is a general-purpose load balancer that happens to support Lambda as a target; it's simpler and often cheaper for straightforward HTTP routing, but it doesn't give you API-management features out of the box.

The practical rule of thumb: if you're building a public or partner-facing API where you need throttling, API keys, or request validation as first-class features, API Gateway saves you from reinventing them. If you just need to put a Lambda function behind a URL as part of a broader system that's already using an ALB for other services, and you don't need API-management features, an ALB integration is lighter weight and cheaper at high request volumes.

  • API Gateway documentation · ALB Lambda targets documentation

S3 now provides strong read-after-write consistency for all requests, including new object PUTs, overwrite PUTs, and DELETEs — as soon as a write succeeds, every subsequent read, in every Region, reflects that write. This became true in December 2020; before that, S3 offered read-after-write consistency only for new object creation, and eventual consistency for overwrites and deletes, meaning a read right after an overwrite could briefly return the old version.

It's a trap specifically because a lot of older tutorials, blog posts, and even some AWS training material still describe the pre-2020 eventual-consistency model, so a candidate who learned from an outdated source will confidently describe a limitation that no longer exists. The safe answer today is simply "S3 is strongly consistent," with the historical context offered only if asked.

  • Amazon S3 consistency model documentation

A Local Secondary Index (LSI) shares the same partition key as the base table but lets you define a different sort key, and it must be created at table creation time — you can't add one later. A Global Secondary Index (GSI) can use a completely different partition key and sort key from the base table, can be created or removed at any time, and is queried like its own separate index with its own provisioned throughput.

The choice mostly falls out of two constraints: if you need to query by a totally different attribute than the base table's partition key, that requires a GSI, since an LSI can't change the partition key. If you need strongly consistent reads on the index, that requires an LSI, since GSIs only support eventually consistent reads. In practice most tables reach for GSIs, since the need to add an index after the table already has data rules out LSIs for anything except access patterns known up front.

LSI GSI
Partition key Same as base table Can differ
Created At table creation only Any time
Read consistency Strong or eventual Eventual only
  • DynamoDB secondary indexes documentation

The shared responsibility model splits security duties between AWS and the customer: AWS is responsible for the security of the cloud — physical data centers, hardware, the virtualization layer, and for managed services, the underlying software too. The customer is responsible for security in the cloud — everything they configure on top of that.

Where that line sits depends on how managed the service is. On EC2, you're responsible for the guest OS, patching, firewall rules (Security Groups), and anything you install — AWS only guarantees the physical host and hypervisor. On RDS, AWS additionally takes over patching the database engine and the underlying OS, so your responsibility shrinks to things like IAM permissions, network access, and the data itself. The trap is assuming a fully managed service means "AWS handles security" full stop — even on RDS, a publicly accessible database with a weak security group or an overly permissive IAM policy is entirely the customer's failure, not AWS's.

  • Shared responsibility model documentation

Encrypting data directly with a KMS key means every encrypt or decrypt call goes to KMS itself, which caps out at 4 KB per request and adds an API call for every operation. Envelope encryption avoids both limits: KMS generates a one-time data key, that data key encrypts the actual data locally (no size limit, no per-record API call), and then only the small data key itself is encrypted by KMS and stored alongside the data.

This is the pattern almost every AWS service uses under the hood — S3 server-side encryption, EBS volume encryption, and the AWS Encryption SDK all work this way — because it means KMS is only ever handling tiny keys, not your actual payloads, while the master key never leaves KMS at all. The follow-up worth knowing: to decrypt, you send the encrypted data key back to KMS, which decrypts it and returns the plaintext data key, which you then use locally to decrypt the data — KMS never sees the data itself, only the wrapped key.

  • AWS KMS envelope encryption documentation

Both let an application fetch a value at runtime via an IAM-authenticated API call instead of hardcoding it, and both can encrypt with KMS — but they're built for different jobs. Secrets Manager is purpose-built for secrets that need automatic rotation, like database credentials, and bills per secret per month plus per API call. Parameter Store is a general-purpose configuration store — API endpoints, feature flags, AMI IDs, license codes — with a free standard tier and no built-in rotation.

The deciding factor is rotation and sensitivity, not just "is this a secret." A database password that should rotate on a schedule belongs in Secrets Manager, since that automatic rotation is the feature actually worth paying for. A stable configuration value, or a secret you're willing to rotate manually, is cheaper and just as functional in Parameter Store's SecureString type. The common wrong answer treats Secrets Manager as strictly "the better version" of Parameter Store — the cost difference is real enough that defaulting every value to Secrets Manager "to be safe" is itself a design mistake, not a safe default.

  • Secrets Manager vs. Parameter Store documentation

Route 53 decides which record to return for a given DNS query, and each policy answers "which one" differently. Weighted routing splits traffic across records by a proportion you assign — useful for canary releases or gradually shifting traffic from one target to another. Latency-based routing returns whichever Region has the lowest measured network latency for the resolver making the request — useful when you run the same service in multiple Regions and want each user hitting the closest one. Failover routing serves a primary record while it's healthy and switches to a secondary only when a health check fails — built specifically for active/passive disaster recovery, not for splitting normal traffic.

The way to choose is to ask what problem you actually have: shifting a known percentage of traffic on purpose is weighted; serving global users from their nearest Region under normal conditions is latency-based; keeping a standby ready for when the primary dies, and otherwise ignoring it, is failover. A common mistake is reaching for latency-based routing to solve a failover problem — it will happily keep sending some traffic to a Region with high latency even if that Region is actually unhealthy, unless you also attach health checks, because "lowest latency" and "is currently working" are two different questions.

  • Route 53 routing policies documentation

ElastiCache is a managed in-memory data store (Redis or Memcached) you put in front of, or alongside, a database to serve frequently requested data from memory instead of hitting disk-backed storage on every request.

It solves the problem of a database being fast enough for its own workload but too slow, or too expensive to scale, for the read volume an application actually generates — a product page that gets read thousands of times for every one time it changes is the textbook case, where caching the rendered result cuts both latency and the number of queries hitting the database. It's also commonly used for things that don't belong in a relational database at all even if they technically could live there, like session state or a leaderboard, where in-memory access patterns fit naturally.

The trade-off worth naming: a cache adds a second place data can be wrong. Once you introduce caching, you've introduced cache invalidation — deciding when a cached value becomes stale and needs to be refreshed or evicted — which is a real design problem, not an afterthought, and picking the wrong invalidation strategy (or none at all) is a common source of "why is the app showing old data" bugs.

  • Amazon ElastiCache documentation

Those numbers point at warm standby, not backup-and-restore and not full active-active. A 1-minute RPO means data loss has to be capped at roughly a minute, which rules out backup-and-restore entirely — periodic backups (even hourly) lose far more than that. A 15-minute RTO is fast enough that pilot light is risky, because pilot light's application tier isn't running at all in the secondary Region, and standing it up plus scaling it to handle full production traffic can easily blow past 15 minutes if capacity isn't guaranteed. Full active-active would comfortably meet both targets but is the most expensive and operationally complex option, and nothing in the requirement calls for it.

A warm standby design looks like this:

  • Data layer: continuous, near-real-time replication to the secondary Region — Aurora Global Database, DynamoDB Global Tables, or S3 Cross-Region Replication depending on what's storing the data — which is what gets the RPO under a minute.
  • Compute layer: a scaled-down but fully functional copy of the application stack already running in the secondary Region, sized to handle health checks and some traffic, not zero.
  • Failover mechanism: Route 53 health checks against the primary Region, with automated DNS failover to the secondary Region's endpoint.
  • On failover: the Auto Scaling group in the secondary Region scales from its reduced baseline up to full production capacity — this is the step that has to complete inside the 15-minute RTO, which is why it's pre-warmed rather than starting from zero.

The trade-off to state explicitly: warm standby costs more than pilot light because you're running live compute in the secondary Region continuously, but it costs meaningfully less than active-active because that compute runs at reduced capacity rather than full production scale, and it's the strategy that actually matches both numbers in the requirement rather than over- or under-shooting one of them.

  • AWS disaster recovery strategies documentation

This is a hot partition. DynamoDB throughput isn't a single shared pool — it's divided across physical partitions, each capped at roughly 3,000 RCU and 1,000 WCU regardless of how much unused capacity exists elsewhere on the table. If one partition key (or a small handful of values) is receiving disproportionate traffic, that partition hits its individual ceiling and starts returning ProvisionedThroughputExceededException on the requests that hit it, even while CloudWatch shows the table's overall ConsumedReadCapacityUnits and ConsumedWriteCapacityUnits nowhere near the provisioned total.

The fix depends on where the skew is coming from:

  1. Confirm it first. CloudWatch's per-partition metrics (or, more directly, checking which keys are being hit) will show one partition dominating traffic. Don't assume — verify.
  2. Fix the key design if the partition key itself has low cardinality — something like status or tenant_id for a tenant with far more traffic than others concentrates writes onto too few partitions. Moving to a higher-cardinality key, or a composite key that spreads a single logical entity's writes across a suffix (write-sharding), distributes the load.
  3. Add a cache in front of hot reads with DAX if the pattern is read-heavy access to a small set of items — a product detail page for a viral item is the classic case.
  4. Let adaptive capacity do its job for short-lived spikes — it automatically shifts throughput toward a hot partition within limits, and often resolves brief imbalances without any schema change at all.

The trap is reaching straight for "just increase provisioned capacity." That raises the table's total ceiling, but if all the traffic is landing on one partition, the extra headroom sits unused everywhere else while the same partition keeps throttling — the fix has to address the distribution of traffic, not just the total.

  • Choosing a partition key documentation

Scope the execution role to exactly the actions and resources the function actually calls: s3:GetObject on the specific bucket and prefix it reads from (not s3:* on *), dynamodb:PutItem (and UpdateItem if it does updates) on the specific table ARN, and sns:Publish on the specific topic ARN, plus the baseline CloudWatch Logs permissions Lambda needs to write logs. If the function only ever reads one prefix in the bucket, scope the resource ARN down to that prefix rather than the whole bucket.

PowerUserAccess is wrong for a few concrete reasons, not just "it's too broad" in the abstract:

  • It grants access to essentially every AWS service and most actions within them, so a bug, a bad input, or a compromised dependency in that one function now has a blast radius covering the entire account's services — not just the three resources it's supposed to touch.
  • It includes IAM read access and, depending on version, some write paths, which is a privilege-escalation risk specifically inappropriate for a function that has no legitimate reason to touch IAM at all.
  • It makes the role useless as documentation. A tightly scoped role is self-describing — reading its policy tells you exactly what the function does. A PowerUserAccess role tells you nothing, and six months later nobody can safely tighten it because nobody knows which of those broad permissions are actually load-bearing.

The senior-level tell in this question is whether the candidate also mentions auditing over time — permissions tend to only grow as functions are extended, so a scoped-down role needs periodic review (via CloudTrail-driven access analysis, for example) to make sure it hasn't quietly drifted back toward "broad, just in case."

  • Lambda execution role documentation

Since CPU rules out the obvious "instances are just overloaded" explanation, work through the layers between the client and the application in order, because a 5xx from an ALB can originate at several different points and each one has a distinct fingerprint.

  1. Separate ALB-origin errors from backend-origin errors first. A 503 with no X-Amzn- request tracing from the target usually means the ALB itself couldn't find a healthy target — check target group health checks. A 502 often means a target returned a malformed response or reset the connection. A 504 means the target didn't respond within the idle timeout. The ALB's own access logs (if enabled) record which of these happened per request, which is the single fastest way to narrow this down instead of guessing.
  2. Check target health check settings against the application's actual behavior under load. If the health check interval and unhealthy threshold are aggressive, an instance that's briefly slow under load (not down, just momentarily slow) can get marked unhealthy, pulled out of rotation, and then flip back healthy once load passes — producing exactly the "intermittent, CPU looks fine on average" symptom, because the CPU graph is smoothing over a spike that only lasted a few seconds.
  3. Check connection draining and deregistration delay during scale-in events — if the Auto Scaling group is terminating instances to scale down while they still have in-flight requests, and the deregistration delay is too short, in-flight requests get dropped, showing up as 5xx that correlates with scale-in events rather than peak load.
  4. Check the ALB's idle timeout against the application's response time for a subset of slow requests — if a handful of requests take longer than the default 60-second idle timeout (a slow downstream call, a cold Lambda if any part of the chain is serverless, a lock contention spike), the ALB will return a 504 for exactly those requests while the rest sail through fine, which again produces an intermittent pattern that a CPU average won't reveal.
  5. Look one level past the instance, not just at it — if the backend is calling a database or another service, exhausted connection pools or a downstream dependency's own throttling can produce errors that look like ALB flakiness but are actually happening inside the request, invisible to instance-level CPU metrics entirely.

The trap is stopping at "CPU is fine, so scale out more" — that's treating a symptom that doesn't fit the actual cause, and in the deregistration-delay and idle-timeout cases, adding more instances won't change the error rate at all.

  • Troubleshooting ALB documentation

For a team new to containers with no existing Kubernetes experience, ECS is usually the better starting point. It's AWS's own container orchestrator, simpler to reason about, has a gentler learning curve, and paired with Fargate removes cluster management entirely — you define a task and AWS runs it, with no EC2 nodes or control plane to patch. EKS is Kubernetes running on AWS, which means the full weight of Kubernetes concepts (pods, deployments, services, ingress controllers, CRDs) on top of AWS-specific integration work, and a real learning curve even before you touch anything AWS-specific.

The case for EKS despite that overhead comes down to portability and ecosystem: if the team needs workloads that could move to another cloud or run on-prem, if they're adopting a lot of the Kubernetes-native ecosystem (Helm charts, operators, service meshes built for k8s), or if there's already Kubernetes expertise elsewhere in the organization, EKS's standardization pays for its complexity. If none of that applies — the team is AWS-only, has no existing Kubernetes skills, and just wants containers running reliably — introducing Kubernetes purely because it's the "more serious" choice is usually the wrong trade, since it adds real operational surface area (even with EKS managing the control plane, node groups, IAM-to-RBAC mapping, and add-ons are still the team's job) without a corresponding benefit for that team's actual requirements.

The strongest answer here explicitly says "it depends on the team, not the technology" — treating ECS as strictly "the easy/lesser option" and EKS as strictly "the correct professional option" is the common wrong instinct, and misses that portability and ecosystem fit are the actual deciding factors, not maturity.

  • Amazon ECS documentation · Amazon EKS documentation

DynamoDB Global Tables is the building block: it replicates a table across multiple Regions, each Region can accept both reads and writes, and changes propagate to every other Region typically within a second. On top of that, Route 53 latency-based or geoproximity routing sends each user to their nearest Region, so writes made in one Region show up in every other Region without any application-level replication code.

The trade-off is a version of the CAP theorem showing up concretely: Global Tables uses last-writer-wins conflict resolution at the item level. If two Regions write to the same item within the propagation window — which is rare for most access patterns, but not impossible — one write silently overwrites the other, based on a timestamp, with no merge and no application-level conflict resolution. That's fine for access patterns where each item is effectively "owned" by one user or one Region (a user's own profile, their own shopping cart), but it's actively dangerous for anything with concurrent multi-writer updates to shared state — a global inventory counter is the canonical example, where two Regions decrementing stock for the same item at nearly the same time can both succeed and leave the count wrong, because last-writer-wins has no idea it should have summed the decrements instead of picking one.

The practical mitigation is architectural, not a DynamoDB setting: design access patterns so each item is normally written from one Region at a time (route a given user consistently to their home Region under normal conditions), and reserve strongly consistent, single-Region coordination — a different data store, or a Region-owned "source of truth" table — for the specific pieces of data that genuinely need multi-writer correctness, rather than assuming Global Tables makes every access pattern safe for concurrent global writes.

  • DynamoDB Global Tables documentation

Treat this as an active security incident the moment it's confirmed, not a cleanup task — a public commit means the keys should be assumed compromised immediately, since scanners actively scrape GitHub for exposed AWS credentials within minutes of a push, often faster than a human notices.

  1. Deactivate or delete the exposed access key immediately in IAM — this is the single action that stops further damage, and it should happen before anything else, including any investigation of what the key was used for.
  2. Rotate any other credentials the compromised key had access to — if it could read a database connection string from Secrets Manager, or assume another role, treat those as potentially exposed too and rotate them, since a key isn't just a door, it's every door it can open.
  3. Check CloudTrail for activity from that access key since it was first exposed — API calls made, resources created, IAM changes attempted (especially anything that tries to create a new access key or attach a policy, which is a common attacker move to establish persistence before the original key gets revoked).
  4. Scan for anything unexpected the key might have created — new EC2 instances (a common abuse pattern is spinning up instances for cryptomining), new IAM users or roles, new access keys on existing users — and remove anything unauthorized.
  5. Remove the secret from the repository properly, not just delete-and-commit — the key is still in git history and in GitHub's cache/forks unless the history itself is rewritten (BFG Repo-Cleaner or git filter-repo) and, ideally, the repo is temporarily made private while that happens.
  6. Only after containment, do the retrospective: how did the key get into a commit in the first place (no .gitignore for a config file? a hardcoded key instead of an environment variable or IAM role?), and whether a secret-scanning pre-commit hook or GitHub's push protection would have caught it before it ever left the engineer's machine.

The trap is starting with step 6 — trying to figure out how it happened, or being lenient because "it was probably fine," before revoking the key. The order matters: contain first, investigate second, prevent-next-time third.

  • Guidance for exposed access keys — AWS re:Post

For a small, stable number of VPCs that need to talk to each other, VPC Peering is simpler and cheaper — no hourly charge for the connection itself, just standard data transfer costs. For anything growing past a handful of VPCs, or needing centralized control, Transit Gateway is almost always the better shape, even though it costs more per attachment.

The reason comes down to two things that don't scale with peering:

  • Connection count. Peering connections are point-to-point and non-transitive — VPC A peered with B, and B peered with C, does not let A reach C. Connecting N VPCs fully requires N×(N-1)/2 individual peering connections, each with its own route table entries on both sides. Four VPCs need six connections; ten VPCs need forty-five. Transit Gateway is hub-and-spoke: every VPC attaches once to the gateway, and the gateway routes between all of them, so adding a new VPC is one attachment, not N-1 new connections.
  • Operational blast radius of growth. Because peering has no transitivity, adding a new VPC to a peered mesh means touching every existing VPC's route tables to add the new pairwise connection — each one a chance to misconfigure a CIDR. With Transit Gateway, adding a new VPC is a single attachment to the hub; nothing about the other VPCs needs to change.

Transit Gateway also adds capabilities peering doesn't have at all — routing between VPCs across Regions via Transit Gateway peering, centralizing egress through one inspection point for a security team that wants all outbound traffic passing through a single firewall fleet, and Resource Access Manager sharing across AWS accounts and Organizations.

The honest trade-off to state: Transit Gateway costs more (per attachment plus data processing) and adds a layer of indirection that a two-VPC peering connection doesn't need. A team with two or three VPCs that will very likely stay that size shouldn't reach for Transit Gateway just because it's the "more scalable" answer — the extra cost and complexity should be justified by an actual growth trajectory, not adopted preemptively.

  • Transit Gateway documentation · VPC Peering documentation

Work through this in the order that actually removes waste, rather than jumping straight to the most dramatic lever:

  1. Find and eliminate pure waste first — idle or oversized instances, unattached EBS volumes still being billed, old snapshots nobody's using, dev/test environments running 24/7 that only need to exist during working hours. AWS Cost Explorer and Compute Optimizer will surface most of this directly, and it's the only step with zero availability risk, since you're removing spend on things doing nothing useful.
  2. Right-size before committing to anything. Compute Optimizer's recommendations, or just CloudWatch CPU/memory history, often show instances running at a fraction of their provisioned capacity — dropping from an oversized instance type to one that actually matches utilization is pure savings with no trade-off, as long as it's based on real usage data and not a guess.
  3. Shift steady-state, predictable workloads to Savings Plans. A Savings Plan is the safer commitment to make first, over Reserved Instances, precisely because it applies automatically across instance families and even to Fargate and Lambda — so if the workload shape changes over the commitment term (a likely scenario over 1-3 years), the discount still applies, whereas an RI locked to a specific instance family doesn't flex with it.
  4. Move fault-tolerant, interruptible workloads to Spot — CI/CD runners, batch jobs, stateless workers behind an Auto Scaling group with an On-Demand baseline and Spot for the burst capacity. This is where the biggest percentage savings live (up to 90%), but it's also the step with real availability risk if applied carelessly: a workload that isn't actually interruption-tolerant, or has no fallback to On-Demand, can turn a 40% cost cut into an outage the day AWS reclaims a large batch of Spot capacity at once.
  5. Only after the above, revisit architecture — serverless for genuinely spiky or low-traffic workloads where paying per-request beats paying for idle capacity, and turning off non-production environments outside business hours with a scheduler.

The trap is going straight to step 4 and moving customer-facing production traffic onto Spot to hit the number fast — that's the one move on this list that can directly cause the availability problem the question explicitly asked you to avoid.

  • AWS Cost Optimization documentation

These three sit on a spectrum where lower cost trades against faster recovery, and the right one depends on how much downtime and data loss the business can actually tolerate — not on which sounds the most "enterprise."

  • Pilot Light keeps only the core of the system — typically just the database, continuously replicated — running in the secondary Region, with everything else (application servers) defined but not running. On failover, you have to provision and scale up the application tier before it can serve traffic, which puts RTO in the tens of minutes at best. It's the cheapest of the three since almost nothing runs continuously in the secondary Region, but it carries a real risk that gets missed in a quick answer: because the compute isn't running, you're not guaranteed the capacity you need is actually available in that Region the moment you need it, unless it's backed by a capacity reservation.
  • Warm Standby runs a scaled-down but fully functional copy of the whole stack continuously in the secondary Region — not just the database. Failover means scaling that existing stack up to full capacity rather than building it from nothing, which gets RTO down to minutes instead of tens of minutes. It costs more than Pilot Light because compute is running around the clock, even at reduced size, but it's also the only one of the two you can realistically test with synthetic transactions on a regular basis, since the stack is always live.
  • Multi-Site Active/Active runs full production capacity in two or more Regions simultaneously, actively serving real traffic in normal operation, not just standing by. This gets RTO and RPO close to zero, since there's no failover step at all — traffic is just rerouted — but it's the most expensive option by a wide margin and the most operationally complex, since it requires the application to handle multi-writer data consistency correctly under normal conditions, not just during a disaster.

The mistake to avoid in an answer here is presenting these as strictly "which is best" — the honest framing is that each one is the right choice for a specific RTO/RPO target and budget, and picking Active/Active by default because it sounds the most robust usually means paying for resilience the business doesn't actually need.

  • AWS disaster recovery strategies documentation

Serverless pricing (Lambda, API Gateway, DynamoDB on-demand) is per-invocation and per-request — you pay for exactly what you use, with zero cost when idle. EC2 pricing is per-instance-hour — you pay for capacity whether or not it's being used at that moment. At low and spiky traffic, that difference strongly favors serverless: an EC2 instance sitting mostly idle, provisioned to handle occasional bursts, is paying for unused capacity around the clock, while Lambda costs nothing between invocations.

That relationship flips at sustained high volume, because the per-request serverless pricing model doesn't have the economies of scale that a committed, fully-utilized EC2 fleet does. A steadily busy EC2 instance running at high utilization, especially covered by a Savings Plan, is being paid for efficiently — every hour of that commitment is doing real work. The same sustained load running through Lambda pays the per-invocation rate on every single request, with no equivalent bulk discount for "this function runs constantly and predictably" the way a Savings Plan rewards steady EC2 usage — so at high, consistent throughput, the always-on unit economics of committed EC2 capacity can undercut the pay-per-request model.

The way to actually decide, rather than picking based on which architecture feels more modern, is to model cost against the traffic shape specifically:

  • Spiky, low-average, or unpredictable traffic → serverless usually wins, because you're not paying for the idle time between spikes.
  • Steady, high, predictable traffic → provisioned EC2 (ideally covered by Savings Plans) usually wins, because you can commit to and fully utilize that capacity.
  • Mixed → many real systems run the steady baseline on EC2/ECS and handle overflow or spiky secondary paths (webhooks, scheduled jobs, low-traffic internal tools) on Lambda, rather than committing the whole system to one model.

The trap in this question is treating "serverless" as synonymous with "cheaper" by default — it's cheaper for a specific traffic shape, and the honest answer names that shape rather than asserting a universal winner.

  • AWS Well-Architected cost optimization pillar

Low CPU with slow queries under load points away from "the database needs more compute" and toward contention, connections, or something happening outside the query itself — so work through those before resizing anything.

  1. Connection exhaustion. A relational database has a hard cap on concurrent connections, and if the application (or a fleet of Lambda functions, which is a classic version of this problem) opens a new connection per request instead of pooling them, requests start queuing or failing to even get a connection long before the database is doing meaningful work — CPU stays low because the database is waiting, not computing. Check the connection count against the instance's max, and check whether the application uses a connection pool (or, for Lambda, RDS Proxy) at all.
  2. Lock contention. A handful of long-running transactions or unindexed writes can hold row or table locks that queue up other queries behind them. CPU stays idle while queries wait on a lock rather than executing, which is easy to mistake for "the database isn't working hard enough to be slow" when it's actually blocked, not busy.
  3. Missing indexes that only bite at volume. A query without a useful index can return fine against a small test dataset via a full table scan, and only become slow once the table has production-scale rows — this shows up as I/O wait rather than CPU, since the bottleneck is reading a lot of disk (or cached) pages, not computing.
  4. Replication lag, if reads are being served from a Read Replica — the replica can be healthy and low-CPU while simply behind the primary, which looks like "slow queries" from the application's point of view even though nothing is actually struggling to execute.

The trap is treating "low CPU + slow" as evidence the database is fine and looking everywhere else first — low CPU with slow response time is itself the diagnostic signal that something is blocking or waiting, not computing, and it should point the investigation at connections, locks, and I/O before anything else.

  • Amazon RDS performance troubleshooting documentation

The core tool for this is AWS X-Ray: it traces a single request end-to-end across every service that supports it, and stitches the segments together into one trace so you can see exactly how long each hop took, rather than piecing it together from separate logs in separate services after the fact.

The design has a few pieces that all need to be in place, not just one:

  • Enable active tracing on API Gateway and on both Lambda functions — this isn't on by default, and a common reason "X-Ray shows nothing" is simply that it was never turned on for one of the hops in the chain.
  • Propagate the trace context across the boundary between the two Lambda functions. If the first function calls the second directly (rather than through API Gateway or another traced service), the X-Ray trace ID has to be passed along explicitly — usually via the invocation payload or headers — or the second function starts a brand-new, disconnected trace instead of continuing the first one.
  • Instrument the AWS SDK calls (the DynamoDB calls in particular) using the X-Ray SDK wrapper, so the time spent waiting on DynamoDB shows up as its own labeled segment in the trace rather than being invisible inside the Lambda function's total duration.
  • Pair traces with CloudWatch Logs and metrics, not instead of them — X-Ray answers "which hop was slow," while CloudWatch Logs answer "why was it slow" (an error message, a retry, a specific input). Structured logs with the trace ID included let you jump from a slow trace straight to the relevant log lines.

The result should be a single trace map showing, for one request, exactly how many milliseconds were spent in API Gateway overhead, in each Lambda function's own code, and waiting on DynamoDB — which is what turns "the API feels slow sometimes" into "the second Lambda function's DynamoDB call is the bottleneck 80% of the time," an actionable finding instead of a guess. The trap is instrumenting only the Lambda functions and skipping the SDK calls inside them — that leaves the exact question the question is asking ("where is the time going") as one unlabeled black box inside each function's segment.

  • AWS X-Ray documentation
Keep going
All interview prep
Quizzes — test what you know
Modules — hands-on lessons
Glossary — quick term lookups