50 real AWS interview questions with detailed answers covering EC2, S3, IAM, VPC, Lambda, DynamoDB and cost optimization — grouped by difficulty.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |
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.
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.
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.
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.
Explicit Deny ─▶ DENY (stop) │ no match ▼Explicit Allow ─▶ ALLOW │ no match ▼ (default) ─▶ DENYAn 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |
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.
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.
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.
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.
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.
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:
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.
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:
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.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.
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:
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."
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.
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.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.
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.
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.
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.
git filter-repo) and, ideally, the repo is temporarily made private while that happens..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.
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:
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.
Work through this in the order that actually removes waste, rather than jumping straight to the most dramatic lever:
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.
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."
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.
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:
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.
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.
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.
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:
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.