An e-commerce on a single shared server works — until the day it matters. A traffic spike (a sale, a post that takes off), a disk failure, a corrupted database: every incident is paid in lost orders. AWS doesn’t make the problem magic, but it gives you the right tool for each risk.
Black Friday, a story that blows up: traffic 10x in an hour. You want to scale up then back down — and pay only for what you use.
The cart and checkout must never go down. Multi-AZ, replicas, automatic failover: one machine dying doesn’t take the shop offline.
Backups, patches, replication: delegated to AWS. You spend your time on the shop, not on sysadmin.
Pay-as-you-go, taggable, optimizable. Done right, you pay less than a big dedicated server idling overnight.
The real win isn’t “cloud is better,” it’s decoupling: each brick (site, database, images) becomes a service you size and secure separately.
Four services, each on its job. A request comes in through the CDN, the site runs on EC2, the data lives in RDS, and photos are served from S3 — optimized along the way by Lambda.
EC2 is a machine you fully control. It’s the right pick when you have legacy PHP (WooCommerce, PrestaShop) or a server-rendered Next.js app: you install what you want, you own the configuration.
The deployment principle (Docker, reverse proxy, HTTPS) is the same as in my deploy Langfuse on AWS EC2 guide. To handle spikes, you put several EC2 behind a load balancer, auto-scaled.
Rule number one: the database does not run on the EC2. A database on the same machine as the site means a disk failure = lost orders, and zero backup the day you need one. RDS runs MySQL or PostgreSQL for you.
Never make RDS publicly accessible. Keep it in the private network (VPC), and only the site’s EC2 can talk to it.
Product images have no business on the EC2 disk: it doesn’t scale, it’s lost on redeploy, and it saturates the machine. They go to S3 — near-indestructible, cheap object storage — and are served through CloudFront.
This is the brick that changes everything on speed. Merchants upload 5 MB JPEGs straight from the phone. You never serve that to the visitor. On every drop into S3, an event triggers a Lambda that converts to WebP and generates several sizes.
Result: a 5 MB image becomes three WebP files of a few dozen KB, served by CloudFront with a one-year cache. The product page goes from slow to instant — with nothing for the merchant to do.
The classic trap: the Lambda writes back into the same bucket, re-triggers itself, and loops forever. Filter on the suffix (here original.jpg), or write the variants to a separate prefix.
A single EC2, even well-sized, eventually saturates on a peak day. The real defense isn’t a bigger machine, it’s an Application Load Balancer in front of an auto-scaling group: traffic spreads across several identical EC2 instances, and the group adds or removes machines on its own based on load.
I drive the group with target tracking: I set a target (say 50% average CPU, or a request count per instance) and AWS creates the CloudWatch alarms that scale capacity up then back down. For an e-commerce, the most meaningful metric is `ALBRequestCountPerTarget` — the average number of requests per instance behind the load balancer. A detail from the AWS docs: set the target as high as possible with a buffer for spikes, and the group keeps just enough machines running.
Classic spike trap: scale-out takes a few minutes (EC2 boot + warmup). If you wait for Black Friday to test, you discover too late that your target is too high. I rehearse the spike with a load test a week before, and I tune the warmup so a still-cold instance isn’t counted yet.
As soon as there are several EC2s, a problem appears: a visitor’s cart must not live in the memory of a single machine, otherwise the next click lands on another instance and the cart is empty. So the session and hot data (catalog, prices) move out of the EC2 into a shared cache. I use Amazon ElastiCache (Redis/Valkey): every EC2 reads and writes the same session, and the most frequent catalog queries no longer hit RDS on every page.
The order of the shock absorbers matters: CloudFront absorbs the static, ElastiCache absorbs the hot dynamic, read replicas absorb the remaining reads, and only the strict minimum (order writes) reaches the RDS primary. Each layer shields the next.
On CloudFront, everything happens in the cache behaviors: a single site doesn’t have one caching behavior, but one per content type. I split into at least two: assets (images, CSS, JS) cached hard, and dynamic pages (cart, account) not cached at all.
WebP images, CSS, JS
anything personal
The exact values of the managed policies are documented: `CachingOptimized` goes up to a one-year max TTL, `CachingDisabled` stays at zero. I lean on them rather than writing my own policies — less config to maintain. Details in the managed cache policies docs.
The whole architecture lives in a VPC split into subnets. The EC2s sit in a public subnet (reachable via the ALB), RDS and ElastiCache in a private subnet with no route to the internet. Security groups do the rest: the RDS SG only accepts port 3306 from the EC2 SG, nothing else.
I never let a card number transit through my EC2s. Payment goes to a PCI provider (Stripe, Adyen…) via a client-side hosted field: the card never touches my infra, so my PCI DSS scope shrinks. When part of it must stay with me, the AWS logic is clear — isolate cardholder data in a dedicated account, separate from the rest, to de-scope the non-PCI side. That’s exactly the approach described in this AWS security post on serverless PCI.
Even with a hosted field, the provider’s script lives in YOUR page. Scripts served from your S3 or EC2 can stay in PCI scope (SAQ A-EP). Check what your provider expects before assuming you’re out of scope.
Get the agent out of the black box: instrument every step, measure cost, and score it with an LLM-as-a-judge.
My first LangGraph agent, and why a good prompt stops being enough once the task gets real.