If your production stack still rides on a single Availability Zone, you don’t have AWS architecture best practices — you have a countdown timer with a logo on it. This is a practical hardening pass through cloudonaut’s well-worn 14-point checklist: multi-AZ, auto recovery, decoupling, stateless servers, least-privilege IAM, encryption, caching, and the honest math on multi-region. No hand-waving, no slideware. Things you can actually go implement before Friday.
Multi-AZ and Auto Recovery: the floor, not the ceiling
Multi-AZ is the cheapest resilience you will ever buy, and it’s table stakes. An Availability Zone is one or more discrete data centers with independent power, cooling, and networking, all sitting inside a single low-latency Region. Spread your compute across at least two — three if the Region offers them — and a single AZ catching fire stops being a resume-generating event.
The good news is that most managed services do auto recovery for you, and they do it well. An Application Load Balancer fronts a stable DNS name and quietly stops routing to an AZ that’s having a bad day. RDS Multi-AZ keeps a synchronous standby in a second AZ and fails over in a minute or two without you touching a thing. Aurora goes further with up to fifteen replicas across AZs. Auto Scaling groups span subnets in multiple AZs and replace dead instances automatically — your job is just to spread the subnets and stop pretending one zone is “probably fine.”
Here’s a minimal Auto Scaling group wired across three AZs so failure is a non-event:
resource "aws_autoscaling_group" "web" { name = "web-asg" min_size = 3 max_size = 12 desired_capacity = 3 health_check_type = "ELB" vpc_zone_identifier = [ aws_subnet.private_a.id, aws_subnet.private_b.id, aws_subnet.private_c.id, ] target_group_arns = [aws_lb_target_group.web.arn] launch_template { id = aws_launch_template.web.id version = "$Latest" }}
Set the health check to ELB, not EC2, so a node that boots but can’t actually serve traffic gets reaped. An instance that’s technically “running” while returning 500s is not your friend.
Stateless servers: kill the snowflake
Auto recovery only works if your servers are disposable. The moment an instance hoards state on its local disk — session data, uploaded files, a cache nobody documented — it becomes a precious snowflake you can’t replace without losing something. That snowflake will melt at the worst possible time.
Push state off the box. Sessions go to ElastiCache or DynamoDB. Uploads go to S3. Shared files go to EFS or FSx. Once the instance holds nothing you can’t recreate, you can scale out under load, roll new versions with zero downtime, and let a failed node get terminated and replaced without a single 3 AM page. Stateless is what makes every other resilience pattern actually pay off.
Decoupling with SQS and EventBridge
Synchronous service-to-service calls are how a single slow component takes down everything upstream of it. Put a buffer between producers and consumers and a downstream hiccup becomes a slightly deeper queue instead of a cascading outage.
SQS is the workhorse for point-to-point work distribution — drop a message, let consumers pull at their own pace, scale workers on queue depth. EventBridge is for fan-out and routing, where one event needs to reach several independent consumers without the producer knowing or caring who’s listening. Reach for SQS when you’re handing off a unit of work; reach for EventBridge when you’re broadcasting that something happened.
import boto3, jsonsqs = boto3.client("sqs")QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/orders"def enqueue_order(order_id, payload): sqs.send_message( QueueUrl=QUEUE_URL, MessageBody=json.dumps({"order_id": order_id, "payload": payload}), MessageAttributes={ "type": {"DataType": "String", "StringValue": "order.created"} }, )
Always pair the queue with a dead-letter queue and a sane maxReceiveCount, so a poison message gets quarantined after a few failed attempts instead of looping forever and dragging the whole consumer pool down with it.

Least-privilege IAM: stop handing out keys to the building
Least privilege is the part of AWS architecture best practices that everyone nods along to and almost nobody actually does. Wildcard Action: "*" on Resource: "*" isn’t a policy, it’s a confession. Workloads should authenticate with IAM roles — never long-lived access keys baked into an AMI or, heaven help us, a Git repo. EC2 uses instance profiles, ECS uses task roles, Lambda uses execution roles. Scope each one to the exact actions and resources it needs and nothing more.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "ReadWriteOwnBucketPrefix", "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::acme-orders/incoming/*" }, { "Sid": "PublishToOrdersQueueOnly", "Effect": "Allow", "Action": "sqs:SendMessage", "Resource": "arn:aws:sqs:us-east-1:123456789012:orders" } ]}
This role can write to one prefix in one bucket and publish to one queue. That’s it. If it gets compromised, the blast radius is a single prefix — not your entire account. Use IAM Access Analyzer to catch the over-broad grants you forgot about, because you absolutely have some.
Encrypt in transit, encrypt at rest
Both are basically free in 2026, so there’s no excuse. In transit: terminate TLS at the ALB or CloudFront with an ACM certificate, and don’t run plaintext on the private hops between services if the data is sensitive. At rest: flip on default encryption everywhere. S3 encrypts new objects automatically, EBS supports account-wide default encryption, and RDS, DynamoDB, and EFS all integrate with KMS. Use customer-managed KMS keys when you need your own rotation schedule and audit trail; otherwise the AWS-managed keys are a perfectly respectable starting point.
Caching, network I/O, and throughput
The fastest network call is the one you never make. Put CloudFront in front of static and cacheable content, and ElastiCache (Redis or Memcached) in front of hot reads so your database isn’t answering the same question ten thousand times a second. Minimize cross-AZ chatter where you can — it adds latency and shows up on the bill — and keep instances and their storage close, with the right instance families and EBS volume types for I/O-heavy workloads. Caching is the rare lever that makes things faster and cheaper at the same time.
Multi-region: when it earns its keep
Multi-region is where teams talk themselves into a budget fire. It is genuinely more complex and more expensive than single-region — data replication, failover orchestration, split-brain edge cases, and duplicate infrastructure that mostly sits idle waiting for a disaster that may never come. Do it only with a real business case: an uptime SLA that a single Region truly cannot meet, hard data-residency rules, or users far enough away that latency is hurting you.
For most workloads, a well-built multi-AZ design inside one Region delivers the availability you actually need at a fraction of the operational cost. If you do go multi-region, start with the cheaper postures — backup-and-restore or pilot-light — and only graduate to active-active when the numbers, not the anxiety, justify it. “We might need it someday” is not a business case. It’s a feeling.
At Enkompass (enkompass.net), this is our home turf. We design AWS architectures that survive contact with production — and the 3 AM page that comes with it. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.
