Back to resources
// architecture · e-commerce

Why AWS for an e-commerce

An online store has to absorb a traffic spike without falling over, and never lose an order. Here’s the architecture I set up: EC2 for the site, RDS for the database, S3 + Lambda for optimized product photos.

GuideJul 8, 2026~8 min · intermediate
EC2RDSS3LambdaCloudFrontWebP
01

Why AWS, and not a plain server

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.

01
The spikes

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.

02
Availability

The cart and checkout must never go down. Multi-AZ, replicas, automatic failover: one machine dying doesn’t take the shop offline.

03
Managed services

Backups, patches, replication: delegated to AWS. You spend your time on the shop, not on sysadmin.

04
Controlled cost

Pay-as-you-go, taggable, optimizable. Done right, you pay less than a big dedicated server idling overnight.

i

The real win isn’t “cloud is better,” it’s decoupling: each brick (site, database, images) becomes a service you size and secure separately.

02

The big picture

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.

// the path of a request
Visitor
client
CloudFront
CDN
EC2
web · PHP/Next
RDS
database
S3
product photos
Lambda
→ WebP + sizes
CloudFront also serves the images from S3
CloudFront — the CDN up front: cache, HTTPS, close to the visitor;
EC2 — the site itself (PHP or Next.js);
RDS — the database, managed;
S3 — product photo storage;
Lambda — image optimization (WebP, multiple sizes).
03

EC2 — the site

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.

shellcopy
# sur l'EC2 : Next.js en production, garde-fou au reboot
$ npm ci && npm run build
$ pm2 start "npm run start" --name shop
$ pm2 save && pm2 startup # relance apres un redemarrage de l'instance
i

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.

04

RDS — the managed database

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.

.envcopy
# la base ne tourne PAS sur l'EC2 — elle est managee par RDS
DATABASE_URL=mysql://app:•••@shop-db.abc123.eu-west-3.rds.amazonaws.com:3306/shop
backupsautomatic snapshots + point-in-time restore
multi-AZa standby replica, automatic failover on outage
read replicasread replicas to absorb browsing spikes
patchsupdates and security handled by AWS
!

Never make RDS publicly accessible. Keep it in the private network (VPC), and only the site’s EC2 can talk to it.

05

S3 — the product photos

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.

upload.tscopy
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "eu-west-3" });
// une photo produit part sur S3 — jamais sur le disque de l'EC2
await s3.send(new PutObjectCommand({
Bucket: "shop-produits",
Key: `produits/${sku}/original.jpg`,
Body: fileBuffer,
ContentType: "image/jpeg",
}));
extreme durability: your originals don’t get lost;
decoupled from the site: redeploying the EC2 doesn’t touch the images;
served by CloudFront: fast everywhere, and it offloads the EC2.
06

Lambda — WebP optimization

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.

optimize.tscopy
// handler Lambda — declenche a chaque upload dans le bucket S3
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import sharp from "sharp";
const s3 = new S3Client({});
export async function handler(event) {
for (const rec of event.Records) {
const Bucket = rec.s3.bucket.name;
const Key = decodeURIComponent(rec.s3.object.key);
if (!Key.endsWith("original.jpg")) continue; // evite la boucle infinie
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
const input = Buffer.from(await obj.Body.transformToByteArray());
// 3 tailles, converties en WebP
for (const w of [400, 800, 1600]) {
const out = await sharp(input).resize(w).webp({ quality: 80 }).toBuffer();
await s3.send(new PutObjectCommand({
Bucket,
Key: Key.replace("original.jpg", `${w}.webp`),
Body: out,
ContentType: "image/webp",
CacheControl: "public, max-age=31536000, immutable",
}));
}
}
}

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.

07

Surviving a Black Friday: ALB, auto scaling and caches

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.

The scaling policy I use

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.

scaling-policy.jsoncopy
# cible : 1000 requetes/instance/minute derriere l'ALB
$ aws autoscaling put-scaling-policy \
--auto-scaling-group-name shop-asg \
--policy-name shop-req-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/shop-alb/abc123/targetgroup/shop-tg/def456"
},
"TargetValue": 1000.0
}'
!

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.

Sessions and state: out of the EC2

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.

sessionscart and login in ElastiCache — any EC2 serves any visitor
cache lectureproduct pages and prices cached — RDS breathes during the spike
read replicasremaining reads go to RDS read replicas, not the primary
stateless EC2no local data: an instance can die without losing anything

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.

08

CloudFront tuning, VPC and payment isolation

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.

Assets → CachingOptimized

WebP images, CSS, JS

managed policy, default TTL 24 h, max up to 365 days
no query string or cookie in the cache key: maximal hit rate
edge Gzip/Brotli compression included
Cart/account → CachingDisabled

anything personal

TTL 0: never cached, every request goes to origin
otherwise a visitor would see someone else’s cart — guaranteed incident
CloudFront still handles HTTPS and routing

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 VPC: nobody talks to RDS from outside

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.

public subnet: ALB + EC2 only;
private subnet: RDS, read replicas, ElastiCache — invisible from the internet;
chained security groups: each brick opens its port only to the brick before it.

Payments: get them out of scope

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.

09

Sources & further reading

01EC2 Auto Scaling — target tracking policiesThe official target-tracking scaling docs, including the ALBRequestCountPerTarget metric used for spikes.02CloudFront — managed cache policiesThe CachingOptimized and CachingDisabled policies with their exact TTLs (up to 365 days).03Amazon ElastiCache — What is ElastiCacheThe managed in-memory cache (Redis/Valkey) I use for sessions and hot data.04AWS Security — streamlining PCI with serverlessHow to isolate cardholder data in a dedicated account to shrink PCI DSS scope.05PCI DSS v4.0 on AWS — Compliance Guide (PDF)The reference AWS guide to understand and implement PCI DSS v4.0 compliance.06sharp — high-performance Node.js image processingThe library (libvips) behind the WebP-conversion Lambda: 4–5× faster than ImageMagick.07web.dev — image performance (WebP/AVIF, LCP)Why serving WebP/AVIF lowers LCP — the speed win from the Lambda brick.

The through-line: decouple. Site on EC2, database on RDS, images on S3 optimized by Lambda — each brick scales, secures and heals on its own. That’s what lets a shop ride a spike without sweating. Then comes the real sport: keeping the bill under control.