AWS Interview Essentials — Cross-Service Decision Matrices
The cross-cutting content interviewers ask about most. Individual service files (Compute/, Databases/, etc.) have the depth; this file is the “which one and why” layer plus the patterns that span services.
1. Messaging: SNS vs SQS vs EventBridge vs Kinesis vs MSK
The first split is message versus stream: a message is consumed and gone, a stream is a log you read at your own offset and can re-read.
Message-based — SQS, SNS, EventBridge:
| SQS | SNS | EventBridge | |
|---|---|---|---|
| Pattern | queue, one consumer | pub/sub fan-out | bus with routing |
| Consumers | competing | a copy each | rule-matched targets |
| Ordering | FIFO queues only | FIFO topics only | no |
| Replay | no, acked is gone | no | archive and replay |
| Filtering | no | subscription policies | rich event patterns |
| Throughput | ~unlimited | ~unlimited | thousands/s |
| Cost | per request | per request | per event published |
Stream-based — Kinesis and MSK, both ordered with replay:
| Kinesis Data Streams | MSK (managed Kafka) | |
|---|---|---|
| Consumers | many, own offset | many, own offset |
| Ordering | per shard | per partition |
| Retention | up to 365 days | configurable |
| Filtering | consumer-side | consumer-side |
| Throughput | shards x 1MB/s in | very high |
| Cost | per shard-hour + payload | per broker-hour |
Latency is milliseconds for all five, so it is not the deciding factor. | Best for | work distribution, buffering | fan-out notifications | event routing across many types/targets, SaaS integration, replay | high-throughput streaming, multiple independent readers, ordered replay | Kafka-compatible streaming, existing Kafka tooling |
Decision shortcut:
- Two services, reliable handoff → SQS.
- Tell N services “this happened” → SNS.
- Route many event types to many targets with filtering / archive-replay / partner SaaS → EventBridge.
- High-throughput ordered stream, replay, multiple independent consumers → Kinesis (or MSK if you need Kafka APIs/tooling).
- “Exactly once across DB + queue” → none of these alone; use transactional outbox + idempotent consumers.
See Application_Integration/ for SQS/SNS/EventBridge depth.
2. Compute: Lambda vs Fargate vs ECS-on-EC2 vs EKS vs EC2 vs App Runner vs Batch
The axis that decides it is who manages the capacity.
You manage nothing — it scales to zero and you pay per use:
| Lambda | Fargate | App Runner | |
|---|---|---|---|
| Model | function, event-driven | container | container, web |
| Max duration | 15 min | unbounded | unbounded |
| Cold start | ms to s | 30-60s task start | yes |
| Scales to zero | yes | no, min 1 task | yes |
| Cost | per invocation + GB-s | per task vCPU/RAM-s | per request + compute |
| Best for | spiky, event-driven, short | services and workers | a simple web app |
You manage nodes — cheaper at steady volume, more to run:
| ECS-on-EC2 | EKS | EC2 | |
|---|---|---|---|
| Model | containers, your nodes | Kubernetes | plain VM |
| Cold start | node + task | node + pod | minutes to boot |
| Ops burden | medium | high | high |
| Cost | EC2, you bin-pack | EC2 + control plane | per instance-hour |
| Best for | steady volume, GPU | K8s shops, multi-cloud | full control, legacy |
Batch sits outside both: queue-driven, scales to zero when the queue empties, and priced per job. Use it for scheduled or parallel work such as rendering and ETL rather than for anything serving requests.
Decision shortcut:
- Event-driven, short, spiky → Lambda.
- Long-running web service / worker, don’t want to manage nodes → Fargate.
- Steady high-volume where per-CPU cost matters, or GPU → ECS-on-EC2 (Spot + Reserved).
- Already K8s / multi-cloud / need the K8s ecosystem → EKS.
- Simple “deploy my container as a web app” → App Runner.
- Big parallel batch jobs → Batch.
See Compute/01_Compare_AWS_Compute_Services/README.md and individual files.
3. Datastore: RDS vs Aurora vs DynamoDB vs ElastiCache vs DocumentDB vs OpenSearch
Relational, when you need joins and transactions:
| RDS | Aurora | |
|---|---|---|
| Type | managed PG/MySQL | AWS-built SQL engine |
| Scaling | vertical + read replicas | storage auto, 15 readers |
| Failover | 60-120s Multi-AZ | ~30s |
| Best for | relational workloads | the same, bigger and faster |
Everything else, chosen by access pattern rather than by data model:
| Access pattern | Consistency | |
|---|---|---|
| DynamoDB | known key lookups | eventual, strong opt-in |
| ElastiCache | get/set, TTL | n/a, it is a cache |
| DocumentDB | document queries | tunable |
| OpenSearch | full-text, aggregations | eventual |
DynamoDB scales horizontally and gives single-digit millisecond latency, but only for access patterns you designed the keys around — ad-hoc queries are the thing it is bad at. ElastiCache is for the hot path: sessions, rate limits, cached reads. OpenSearch also serves vector search, which is why it appears in RAG designs on AWS.
Decision shortcut:
- Relational, joins, transactions, moderate scale → RDS.
- Same but need faster failover, more read replicas, or storage > 64 TB → Aurora.
- Known access patterns, massive scale, predictable latency → DynamoDB.
- Sub-ms cache in front of any of the above → ElastiCache.
- You have MongoDB code → DocumentDB (or self-host / Atlas).
- Full-text search, log analytics, vector search → OpenSearch.
4. Request flow walkthrough
A typical production request, with the layers identified:
User
│
▼
Route 53 DNS resolution; latency/failover routing
│
▼
CloudFront edge cache; static assets served here; TLS termination
│ (cache miss / dynamic)
▼
AWS WAF rate limiting, SQLi/XSS rules, geo / IP blocking
│
▼
ALB or API Gateway
│ ALB: L7 routing, ECS/EKS targets, OIDC auth
│ API GW: REST/HTTP API, Cognito/Lambda authorizers, throttling, usage plans
▼
Compute ECS/Fargate task OR Lambda
│ - auth already validated at the edge / gateway
│ - app reads secrets from Secrets Manager / SSM at cold start
▼
Data layer
│ - RDS via RDS Proxy (Lambda) or direct pool (long-running)
│ - DynamoDB direct
│ - ElastiCache checked first (cache-aside)
▼
Async side-effects → SQS / SNS / EventBridge → workersWhere caching lives: CloudFront (edge), ElastiCache (app data), API Gateway cache (rarely worth it). Where auth lives: WAF (coarse), gateway authorizer (token validation), app (fine-grained authz). Observability: CloudWatch Logs/Metrics + X-Ray traces threaded through every hop.
5. The “Lambda + RDS” problem
The single most-asked AWS+backend interview question.
The problem: Lambda scales by spawning concurrent execution environments. Each one opens its own DB connections. 1000 concurrent Lambdas → 1000 Postgres connections → RDS hits max_connections, refuses new connections, the whole service fails. Postgres connections are expensive (a process + memory each); a db.t3.medium caps around ~340 connections.
Three solutions:
| Solution | How | Trade-off |
|---|---|---|
| RDS Proxy | managed connection pooler in front of RDS; Lambda connects to the proxy, proxy multiplexes a small pool to RDS | best general fix; ~$0.015/h per vCPU of the DB; pinning — prepared statements, session-level SET, advisory locks, LISTEN/NOTIFY force a connection to “pin” and stop multiplexing |
| Careful concurrency control | set Lambda reserved concurrency low enough that max concurrent × connections-per-Lambda ≤ RDS capacity; reuse the connection across invocations (open it module-level, outside the handler) | caps throughput; brittle as the system grows |
| Move to DynamoDB | DynamoDB has no connection concept — it’s an HTTPS API; scales with Lambda naturally | only works if the access pattern fits a key-value/document model; not a drop-in for relational workloads |
In an interview, name all three and the pinning gotcha on RDS Proxy. The “right” answer is usually RDS Proxy, with reserved concurrency as a stopgap and DynamoDB as the answer if the data model allows it.
6. IAM patterns for containerized + serverless apps
How code gets AWS permissions without long-lived keys:
| Workload | Pattern | Mechanism |
|---|---|---|
| Lambda | execution role | role attached to the function; SDK picks it up automatically |
| ECS task | task role (app identity) + execution role (ECS infra: pull image, fetch secrets, write logs) | two distinct roles — common confusion point |
| EC2 | instance profile | role attached to the instance; SDK reads it from instance metadata |
| EKS pod | IRSA or EKS Pod Identity | ServiceAccount annotated with a role ARN; SDK assumes it via OIDC. Pod Identity (newer) is simpler — no OIDC provider per cluster |
| CI/CD (GitHub Actions) | GitHub OIDC federation | GitHub’s OIDC token is exchanged for AWS temp creds via an IAM role with a trust policy scoped to the repo; no long-lived AWS_ACCESS_KEY_ID secret |
| Cross-account | assume-role | role in account B with a trust policy allowing a principal in account A; sts:AssumeRole |
The unifying principle: every workload gets a role, not keys. Keys (AWS_ACCESS_KEY_ID/SECRET) are a smell — they don’t rotate, they leak, they can’t be scoped per-request. See Security_Identity_and_Compliance/02_.../02_advanced_role_patterns.md.
7. Cost gotchas reference
The bills that surprise teams:
| Gotcha | Why it bites | Mitigation |
|---|---|---|
| NAT Gateway data processing | ~$0.045/GB processed on top of the hourly charge — a Lambda pushing TBs to S3 from a private subnet pays this | VPC gateway endpoint for S3/DynamoDB (free); interface endpoints for other services |
| CloudWatch Logs ingestion | ~$0.50/GB ingested — verbose logging dominates the CloudWatch bill, not storage | log at INFO not DEBUG in prod; sample; set retention; ship high-volume logs elsewhere |
| CloudWatch custom metric cardinality | each unique dimension combination is a separate metric (~$0.30/metric/mo) — user_id as a dimension = millions of metrics |
never put high-cardinality values in dimensions; use EMF + Logs Insights for that |
| Cross-AZ traffic | ~$0.01/GB each direction between AZs — chatty multi-AZ services pay constantly | AZ-aware routing; co-locate chatty components; it’s the price of HA |
| S3 request costs at scale | GET/PUT are cheap individually but millions of small-object requests add up; LIST is pricier | batch; use larger objects; CloudFront in front; S3 Inventory instead of LIST |
| KMS per-request charges | ~$0.03 per 10k requests — a hot path calling Decrypt per request adds up |
use envelope encryption (decrypt the data key once, cache it); KMS data key caching |
| Idle NAT Gateway / NLB hourly | ~$32/mo each just for existing, before any traffic | delete unused ones; consolidate; question whether you need a NAT GW per AZ in dev |
| Provisioned concurrency / idle Aurora | pay 24/7 even at zero traffic | only for latency-critical paths; Aurora Serverless v2 scales down but not to zero |
8. boto3 idioms
Python-specific patterns interviewers expect:
import boto3
from botocore.config import Config
# Client/session reuse — create once, not per request.
# In Lambda: module-level, outside the handler.
session = boto3.Session()
config = Config(
# adaptive = client-side rate limiting
retries={"max_attempts": 5, "mode": "adaptive"},
connect_timeout=3,
read_timeout=10,
)
s3 = session.client("s3", config=config)
# Paginators — never assume one response has everything
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="my-bucket", Prefix="logs/"):
for obj in page.get("Contents", []):
process(obj)
# Streaming — don't load whole objects into memory
obj = s3.get_object(Bucket="b", Key="big.json")
for line in obj["Body"].iter_lines():
process(line)
s3.upload_fileobj(file_like, "b", "key") # streams the upload
s3.download_fileobj("b", "key", file_like) # streams the download
# Async — for async apps, use aioboto3 / aiobotocore
# import aioboto3
# async with aioboto3.Session().client("s3") as s3:
# await s3.put_object(...)Key points:
- Reuse clients/sessions — client creation is expensive (loads service models). In Lambda, module-level.
retries.mode="adaptive"— client-side rate limiting that backs off when AWS throttles; better than the default “legacy” mode.- Always paginate —
list_*APIs cap results; a paginator handles continuation tokens for you. - Stream large payloads —
upload_fileobj/download_fileobj/iter_linesinstead of reading whole objects. - Set timeouts via
botocore.config.Config— defaults are generous; a hung AWS call shouldn’t hang your request. - Async —
aioboto3(wrapsaiobotocore) for asyncio apps; don’t call sync boto3 in an async handler withoutrun_in_executor.
Interview angle 6
- “SNS vs SQS vs EventBridge?” — SQS = queue/work distribution; SNS = pub/sub fan-out; EventBridge = routing many event types to many targets with filtering, archive/replay, and SaaS integration. Add Kinesis when you need ordered high-throughput streaming with replay.
- “Lambda vs Fargate vs EKS?” — Lambda for event-driven/spiky/short; Fargate for long-running services without node management; EKS when you’re already K8s or need the ecosystem. Cost and ops burden rise left to right.
- “How do you connect Lambda to RDS at scale?” — RDS Proxy (watch pinning), reserved concurrency as a cap, or DynamoDB if the data model fits. The connection-storm problem is the thing being tested.
- “How does your container/CI get AWS permissions?” — roles, never keys: task role for ECS, IRSA/Pod Identity for EKS, GitHub OIDC for CI. Long-lived access keys are a red flag.
- “What’s a surprising AWS bill?” — NAT Gateway data processing, CloudWatch Logs ingestion, custom-metric cardinality, cross-AZ traffic. Knowing these signals real operational experience.
- “boto3 best practices?” — reuse clients, paginate everything, adaptive retries, stream large objects, set timeouts via
Config,aioboto3for async.