$ duy_
cd ../blog

23 Aug 2026 · 23 min read

Security that matches your traffic

Tutorials put WAF, Origin Shield and Shield Advanced in front of sites that get four thousand visits a month. This works out, with verified August 2026 prices, the traffic level at which each layer starts paying for itself — and shows that for a portfolio the honest answer is a hosted zone and nothing else.

  • aws
  • cloudfront
  • s3
  • cost
  • security

Search for “secure static site on AWS” and you will find an architecture diagram with CloudFront, AWS WAF, Origin Shield, Shield Advanced and a Firewall Manager policy, drawn for a site that gets four thousand visits a month. Every box is a real control. Every box is also a line item, and three of those four boxes cost more per month than the entire rest of the stack costs per year.

That is a scalpel bill for a paper cut.

I do not think the answer is “skip security”. The answer is that a control has a price and a threat model, and the two have to be in the same room. So this post works the arithmetic: at what traffic level does each layer start paying for itself?

Every price below was checked against AWS’s own pricing and documentation pages in August 2026, and the sources are linked inline. AWS pricing moves — the flat-rate plans in the middle of this post did not exist eighteen months ago — so treat these as a method you can re-run, not a table you can trust forever. Run your own numbers through the AWS Pricing Calculator before you commit to anything.

The baseline, which is also the cheapest thing on the list

Start here, because the baseline is the hook. The site you are reading is Route 53 in front of CloudFront in front of a private S3 bucket. There is no server, no load balancer, no container, no NAT gateway. Terraform builds it; GitHub Actions publishes to it.

viewer → Route 53 (alias) → CloudFront (OAC, SigV4) → private S3 bucket

The bill, per month:

Line itemCostWhy
Route 53 hosted zone$0.50flat, per zone (Route 53 pricing)
Route 53 queries$0.00alias records to CloudFront are not billed
CloudFront data transfer$0.00inside the 1 TB always-free tier
CloudFront requests$0.00inside the 10,000,000 always-free requests
CloudFront Functions$0.00inside the 2,000,000 always-free invocations
CloudFront invalidations$0.001,000 free paths a month
S3 storage< $0.01the whole site is under a megabyte
S3 GET requests< $0.01only on an edge cache miss
ACM certificate$0.00public certificates are free

About fifty-five cents a month, and DNS is essentially the entire bill. That is not a promotional rate that expires. It is the shape of the architecture.

Two things about that table are worth being pedantic about, because they are the two most commonly confused numbers in AWS.

The CloudFront always-free tier and the new-account free tier are different things. The 1 TB of data transfer out, 10 million HTTP/HTTPS requests and 2 million CloudFront Functions invocations per month are listed under Always Free on the CloudFront pay-as-you-go pricing page — they do not expire after twelve months, and they are not a new-account promotion. Separately, AWS restructured its new-account free tier in July 2025: accounts opened after 15 July 2025 get up to $200 in credits over a six-month plan rather than the old twelve-month trial allowances. Accounts created before that date stay on the legacy programme. The 30-plus always-free offers survive both.

The practical consequence: a five-year-old account and a five-day-old account both get the 1 TB. If someone tells you the CloudFront free tier “runs out after a year”, they are describing a different tier.

Alias records are free to query; CNAMEs are not. Route 53 charges $0.40 per million standard queries for the first billion, but “queries for Alias records are provided at no additional cost” when they point at CloudFront, S3, ELB and friends. The Terraform for this site therefore uses A/AAAA alias records rather than a CNAME to the distribution domain:

resource "aws_route53_record" "site" {
  for_each = local.alias_records

  zone_id = local.zone_id
  name    = each.value.name
  type    = each.value.type

  alias {
    name                   = aws_cloudfront_distribution.site.domain_name
    zone_id                = aws_cloudfront_distribution.site.hosted_zone_id
    evaluate_target_health = false
  }
}

That is a free decision that stays free at a billion queries. Most of the good ones are like that.

The delivery pipeline, also aimed at zero

Nothing in the deploy path costs money either, and the reason is worth stating: the expensive parts of CI/CD are compute minutes and credential management, and both have a free answer at this size.

Compute. A public GitHub repository gets free Actions minutes; GitLab SaaS gives free CI minutes on its free tier; a self-hosted GitLab CE runner on a machine you already own costs whatever that machine already costs. This site uses GitHub Actions because the repository is public. The argument below is identical on GitLab CI — only the token issuer changes.

Credentials. Do not put an access key in CI. Federate. The runner presents a short-lived OIDC token, AWS exchanges it for a session, and there is no long-lived secret anywhere to leak, rotate, or find in a git history two years later:

condition {
  test     = "StringEquals"
  variable = "token.actions.githubusercontent.com:aud"
  values   = ["sts.amazonaws.com"]
}

# Pinned to one repository and one branch. A fork or a pull-request build
# gets a different `sub` and cannot assume this role.
condition {
  test     = "StringEquals"
  variable = "token.actions.githubusercontent.com:sub"
  values   = ["repo:${var.github_repository}:ref:refs/heads/${var.github_branch}"]
}

The sub condition is the load-bearing line. Without it, the trust policy says “any GitHub Actions workflow in the world may assume this role”, which is a materially worse position than the access key you were trying to avoid. GitLab’s equivalent claim is project_path plus ref, and it deserves the same pinning.

The IAM policy attached to that role is PutObject/DeleteObject/GetObject on the bucket, ListBucket on the bucket, and CreateInvalidation on exactly one distribution ARN. Nothing else. A deploy role that can create distributions is not a deploy role.

Cache headers belong to the upload. CloudFront’s Managed-CachingOptimized policy respects whatever the origin sends, so the deploy is what sets caching policy — and it has to set two different ones:

# Content-hashed assets: cache forever, and upload them first so no HTML
# can reference a file that is not there yet.
aws s3 sync dist/ "s3://$BUCKET/" --exclude '*' --include '_astro/*' \
  --cache-control 'public,max-age=31536000,immutable'

# HTML: always revalidate, so a deploy is visible immediately.
aws s3 sync dist/ "s3://$BUCKET/" --exclude '_astro/*' --delete \
  --cache-control 'public,max-age=0,must-revalidate'

Now the invalidation, where the folklore is wrong in an interesting way. The widely repeated claim is that /* will eat your free invalidation allowance. It will not. AWS’s documentation is explicit:

The first 1,000 invalidation paths that you submit per month are free… A path that includes the * wildcard counts as one path even if it causes CloudFront to invalidate thousands of files.

Pay for file invalidation

So /* costs one path out of a thousand, the same as /index.html. Deploy thirty times a day and you are still inside the free allowance. Quota is not the argument.

The real argument against /* is cache warmth. If your assets are content-hashed — app.C4kZ9x2p.css — then a new build produces new filenames, and the old ones are already irrelevant. They cannot be stale, because nothing references them any more. Invalidating /* evicts every one of those still-valid objects from every edge location, and the next few thousand viewers pay a cache miss and an origin fetch for content that had not changed. You spent cache hit ratio to solve a problem you had already solved with filenames.

The proportionate version invalidates only what can actually be stale:

aws cloudfront create-invalidation \
  --distribution-id "$DISTRIBUTION_ID" \
  --paths '/' '/index.html' '/blog/*' '/sitemap-index.xml' '/rss.xml'

Five paths, still free, and every fingerprinted asset stays hot at the edge.

I will be honest that this site currently ships /* anyway. With under a megabyte of content and a handful of deploys a week, the cold-cache cost of a full invalidation is a few cents of origin GETs and a slightly slower first request in each region — genuinely below the noise floor. That is the same proportionality argument the rest of this post makes, pointed at my own pipeline. At a site with gigabytes of assets and a deploy per merge, the selective list stops being a nicety.

The security that costs nothing and applies at every tier

Before any of the paid layers, there is a set of controls whose price is zero at every traffic level. These are not “tier 1” decisions. They are decisions.

Origin Access Control, not the legacy Origin Access Identity. OAC makes CloudFront sign its origin requests with SigV4, and the bucket policy trusts the CloudFront service, narrowed to one distribution:

statement {
  sid     = "AllowCloudFrontOAC"
  effect  = "Allow"
  actions = ["s3:GetObject"]

  principals {
    type        = "Service"
    identifiers = ["cloudfront.amazonaws.com"]
  }

  resources = ["${aws_s3_bucket.site.arn}/*"]

  condition {
    test     = "StringEquals"
    variable = "AWS:SourceArn"
    values   = [aws_cloudfront_distribution.site.arn]
  }
}

The SourceArn condition is the part people drop. Without it the policy reads “any CloudFront distribution may read this bucket” — including one in a stranger’s account, who can then serve your content under their domain and their headers. OAI is also on AWS’s own list of legacy features you must migrate away from, which is a second reason.

Block Public Access on all four settings, and ACLs disabled entirely. BucketOwnerEnforced ownership means there is no ACL surface to misconfigure. Not “no public ACLs” — no ACLs.

A deny for plaintext transport, which costs one policy statement:

statement {
  sid     = "DenyInsecureTransport"
  effect  = "Deny"
  actions = ["s3:*"]

  principals {
    type        = "AWS"
    identifiers = ["*"]
  }

  resources = [
    aws_s3_bucket.site.arn,
    "${aws_s3_bucket.site.arn}/*",
  ]

  condition {
    test     = "Bool"
    variable = "aws:SecureTransport"
    values   = ["false"]
  }
}

Versioning, with a lifecycle rule. Versioning is the rollback story: a bad deploy is one object restore away rather than a rebuild-and-pray. The lifecycle rule expiring noncurrent versions after thirty days is what stops the rollback story from quietly becoming a storage bill.

SSE-S3 rather than SSE-KMS. Same encryption at rest. KMS bills per request, and CloudFront reads this bucket on every cache miss. For public content that nobody needs a separate key policy over, the KMS charge buys a line item and nothing else.

Security headers as a response headers policy. HSTS, a real CSP, frame-ancestors 'none', nosniff, a referrer policy. Applied at the edge, no origin involvement, no charge. If you have not counted headers as a security layer, count them — for a static site they cover a larger share of the realistic attack surface than a WAF does.

Everything above is free and permanent. Now the layers that cost money.

Tier 1 — a portfolio, under about 10,000 visits a month

Ship: the baseline architecture, Shield Standard (automatic), a tight cache policy, the free controls above. Do not ship: AWS WAF, Origin Shield, Shield Advanced. Cost: about $0.55 a month.

The reasoning is a threat model, not a budget. At this tier you are not a target; you are weather. What actually arrives is scrapers, vulnerability scanners spraying /wp-login.php at every IP in a /16, and bots looking for exposed .env files. A CDN in front of a bucket containing nothing but HTML absorbs all of it by construction. There is no PHP to exploit, no database to inject into, no login to brute-force, and no origin reachable except through a SigV4-signed request from one distribution.

The one thing that does matter here is cache hit ratio, because a low hit ratio converts bot traffic into origin requests. Managed-CachingOptimized does not forward cookies or query strings into the cache key, which means ?utm_source=twitter, ?fbclid=... and ?ref=hn all collapse onto the same cached object instead of producing a distinct cache entry and a distinct origin fetch each. That is a free control that is doing more work at this tier than a WAF would.

A WAF here would cost $8 a month against a $0.55 bill — a 1,500% increase — to block requests that were already going to 404 out of a static bucket. It would also be the only component in the stack with a rule set that can break the site.

The plot twist: AWS changed the arithmetic in November 2025

There is a genuinely new option that a 2024-vintage tutorial cannot know about. On 18 November 2025 AWS launched CloudFront flat-rate pricing plans — Free ($0), Pro ($15), Business ($200) and Premium ($1,000) per month — which bundle the CDN with AWS WAF, DDoS protection, Route 53 DNS, CloudWatch Logs ingestion, edge compute and S3 storage credits, with no overage charges.

The $0 tier is the interesting one:

Free plan includesAllowance
Requests1,000,000 / month
Data transfer100 GB / month
AWS WAF rules5 (custom + AWS Managed Rules)
Always-on DDoS protectionyes
Route 53 hosted zone, records and queriescovered when the zone is attached
S3 Standard storage credits5 GB

Read that fourth row again. The Free plan covers the hosted zone fee, which is this site’s entire bill. A portfolio that fits inside 1 M requests and 100 GB could run at genuinely $0.00 a month and pick up five WAF rules it was not going to pay for.

The catch is real, though, and it cuts both ways:

  • The plan’s allowances are smaller than the always-free tier. 1 M requests and 100 GB versus 10 M requests and 1 TB. AWS is explicit that allowances “are not hard limits” and that there are no overage charges, but sustained excess over two to three months may result in AWS serving your traffic “from fewer or more distant edge locations”. So it is not a cap; it is a soft ceiling enforced with latency rather than money. For a growing site that is a trade you should make deliberately.
  • Eligibility is narrower than it looks. One apex domain per plan, at most three Free plans per account, and accounts currently on the AWS Free Tier plan are not eligible.
  • Some features are excluded. Real-time access logs, continuous deployment and staging distributions, multi-tenant distributions, Anycast IP lists, dedicated IP SSL and WAF rule groups are all unsupported; a distribution using them must stay on pay-as-you-go. A CloudFront Function or web ACL already attached to another distribution has to be duplicated rather than shared.
  • A web ACL becomes mandatory. Subscribe to a plan and you must have one associated; you cannot remove it without leaving the plan.

I have not moved this site onto a plan yet, and the /infra page’s costed table describes the pay-as-you-go model it actually runs on today. Both numbers are correct — they are prices for two different products. What the plan changes is the shape of the tier-1 advice: “do not buy WAF at this size” is now better stated as “do not buy WAF at this size; check whether AWS will give you five rules for nothing.”

Tier 2 — a startup or small business, 10,000 to 1,000,000 visits a month

Add: AWS WAF with a rate-based rule, billing alarms, a budget. Cost: roughly $8 to $15 a month on pay-as-you-go, or the $15 Pro plan.

Somewhere in this band the threat model changes, and it changes for an unglamorous reason: you now have something worth taking. A signup form to spam. A search endpoint that costs you money per query. A price list a competitor wants scraped hourly. Credential stuffing against a login. These are application-layer problems, and Shield Standard is not an application-layer control.

AWS WAF pricing, as of August 2026, has three parts:

ComponentPrice
Web ACL$5.00 per month
Each rule$1.00 per month
Each rule group / managed rule group$1.00 per month
Requests$0.60 per million

Monthly fees are prorated hourly, and prices can vary by Region. Take a modest starting set — one rate-based rule plus two AWS Managed Rules groups (Amazon IP reputation list, known bad inputs) — for $5 + $1 + $2 = $8.00 fixed. Then the request charge, which is where the interesting arithmetic lives.

You have to convert visits to requests, and the ratio matters more than people expect. A lean static page is four to six HTTP requests; a typical site with web fonts, an analytics tag and a dozen images is twenty to forty. I will use 10 requests per visit below and recommend you measure your own, because a 4× error in this ratio is a 4× error in the WAF bill.

Requests / month≈ visitsWAF fixedWAF requestsWAF totalCloudFront requests (PAYG)
100,00010 k$8.00$0.06$8.06$0.00 (free tier)
1,000,000100 k$8.00$0.60$8.60$0.00 (free tier)
10,000,0001 M$8.00$6.00$14.00$0.00 (free tier)
50,000,0005 M$8.00$30.00$38.00$40.00
100,000,00010 M$8.00$60.00$68.00$90.00

(CloudFront requests are $0.0100 per 10,000 in North America, Europe and Asia Pacific after the first 10 million, per the pay-as-you-go page — so $1.00 per million billable.)

Three things fall out of that table.

WAF is dominated by its fixed cost until about 10 million requests a month. Below that, you are paying $8 for the privilege of having a web ACL, and the per-request charge is rounding error. This is why WAF feels absurd at tier 1 and reasonable at tier 2: the same $8 divided across 100 k requests versus 10 M requests is a completely different unit economics story.

The crossover with the Pro plan lands almost exactly at 10 million requests. At that volume, pay-as-you-go is $0.50 (hosted zone) + $0.00 (CloudFront, still inside the free tier) + $14.00 (WAF) = $14.50. The Pro plan is $15.00 and covers the CDN, WAF with 25 rules rather than 3, the hosted zone, CloudWatch Logs ingestion, 50 GB of S3 credits, and — the part you cannot buy separately — no overage, ever. Fifty cents for the removal of bill-shock risk is not a hard call. Above 10 M requests, pay-as-you-go loses outright: at 20 M requests it is roughly $30 against the Pro plan’s flat $15.

Blocked requests are billed on pay-as-you-go and not on a plan. WAF’s $0.60 per million applies to requests inspected, including the ones you block. So a bot flood raises your WAF bill in proportion to the flood. On a flat-rate plan, AWS states that “blocked DDoS attacks and requests blocked by AWS WAF never count against your usage allowance”. That difference is invisible on a normal Tuesday and very visible on a bad one.

The rule that earns its keep first is the rate-based one, because it needs no knowledge of your application:

resource "aws_wafv2_web_acl" "site" {
  name  = "site"
  scope = "CLOUDFRONT" # web ACLs for CloudFront live in us-east-1

  default_action {
    allow {}
  }

  rule {
    name     = "rate-limit-per-ip"
    priority = 0

    action {
      block {}
    }

    statement {
      rate_based_statement {
        # Valid evaluation windows are 60, 120, 300 and 600 seconds.
        # 300 is the default; 60 reacts faster and is noisier.
        evaluation_window_sec = 300
        limit                 = 2000
        aggregate_key_type    = "IP"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "rate-limit-per-ip"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "site"
    sampled_requests_enabled   = true
  }
}

Two details from the rate-based rule documentation that catch people out: the evaluation window is not the check interval — AWS WAF checks the rate frequently and independently, the window only says how far back it looks — and rate limiting is applied near your limit, not exactly at it. Set the limit where a human user could never plausibly land, then watch the sampled requests before you switch the action from count to block. A rate limit tuned by guesswork blocks your own RSS reader.

And the thing that matters more than any of it at this tier: billing alarms. The failure mode that actually hurts a small business on AWS is not a breach, it is a $4,000 invoice from something that ran all month unnoticed. AWS Budgets lets you monitor and receive notifications free of charge, with the first two action-enabled budgets free and $0.10 per day for each one after that. CloudWatch’s free allowance covers ten standard-resolution alarm metrics. Set a budget at 150% of expected spend, an alarm on CloudFront BytesDownloaded, and one on WAF BlockedRequests — the last one is how you learn you are being probed without paying for a dashboard product to tell you.

Tier 3 — serious production, over a million visits a month or a real SLA

Consider: Origin Shield, if the origin is expensive. Shield Advanced, if you are actually a target.

Origin Shield is a caching feature, and its value is proportional to origin cost

Origin Shield adds a caching layer between CloudFront’s regional edge caches and your origin, so that a miss propagating from many regions collapses into as few as one origin fetch. It is billed per request that reaches it as an incremental layer: $0.0075 per 10,000 requests in the US and $0.0090 per 10,000 in Europe, Japan, Australia, Singapore, South Korea and India. Requests that route to the regional edge cache in the same Region as Origin Shield skip it and are not charged.

Now do the comparison that decides it. Origin Shield in Singapore is $0.90 per million requests. An S3 GET request is $0.0004 per 1,000 — $0.40 per million.

Origin Shield costs more than twice what the origin request it is preventing costs. Even if it collapsed every duplicate perfectly and drove your S3 GET bill to zero, you would be paying $0.90 per million to avoid $0.40 per million. Data transfer from S3 to CloudFront is already free, so there is no egress saving to recover the difference either. For a static site on an S3 origin, Origin Shield is not a cost optimisation. It cannot be. The arithmetic does not permit it.

This is exactly what AWS’s own guidance says, without the numbers attached. Origin Shield is for “origins that provide just-in-time packaging for live streaming or on-the-fly image processing”, “on-premises origins with capacity or bandwidth constraints”, and multi-CDN architectures — and it “may not be a good fit” for “dynamic content that is proxied to the origin, content with low cacheability, or content that is infrequently requested”.

The pattern: Origin Shield converts origin requests into cheaper Origin Shield requests. If your origin request costs $0.40 per million, there is nothing to convert. If it costs a Lambda invocation, an image transform, a database query, or bandwidth on a rack in a datacentre you pay rent for, the conversion is enormously profitable. Value proportional to origin cost, not to traffic. Traffic is only the multiplier.

Shield Advanced is a purchase with a comma in it

AWS Shield Advanced is $3,000 per month with a 1-year subscription commitment, billed per payer account, plus usage-based data transfer fees ($0.025 per GB for CloudFront). AWS states plainly that “all AWS Shield Advanced benefits, including DDoS cost protection, are subject to your fulfillment of the 1-year subscription commitment.”

That is a $36,000 minimum commitment. This site’s hosting costs $6.60 a year. Shield Advanced would be roughly 5,400 times the annual bill for the thing it protects.

So the honest framing is not “Shield Advanced is expensive”. It is that Shield Advanced is priced for organisations where an hour of downtime costs more than $3,000, and where someone can name the adversary. Fintech during a fundraise. A government service during an election. A gaming platform on launch night. An exchange. If you cannot say who would bother, you are buying insurance against a risk you have not characterised.

What you get for it, per the Shield Advanced documentation, is worth knowing precisely, because two of the items are genuinely hard to replicate:

  • Standard AWS WAF fees are covered for protected resources — the web ACL charge, the per-rule charge, and the base per-million request charge, up to 1,500 WCUs and the default body size, with up to 50 billion requests per calendar month. Bot Control, CAPTCHA and oversized-body inspection are not covered. At very large request volumes this materially offsets the subscription.
  • The Shield Response Team, 24/7, which requires Business or Enterprise Support on top.
  • Automatic application-layer DDoS mitigation, which adds a rule group consuming 150 WCUs.
  • DDoS cost protection — and this is the one that belongs in a cost article.

DDoS cost protection is the Advanced-only feature that a cost argument has to respect

AWS’s Shield FAQ describes it as protecting “your AWS bill against higher fees due to usage spikes from protected Amazon EC2, Elastic Load Balancing, Amazon CloudFront, AWS Global Accelerator, and Amazon Route 53 during a DDoS attack”. It is a Shield Advanced benefit, and you request the credits through AWS Support.

Which means: under Shield Standard, a volumetric attack against your CloudFront distribution is absorbed — and billed. The availability protection is free; the invoice protection is not. For a small site inside the always-free tier, an attack has to be large and sustained to push you out of 1 TB, and if it does, the resulting bill is a conversation with AWS Support that usually ends well but is not a contractual entitlement.

This is the strongest argument for the flat-rate plans at the small end, and I want to give it its due: a plan with no overage charges gives you a bounded bill during an attack for $0 or $15 a month, where the equivalent contractual guarantee on pay-as-you-go costs $3,000 a month with an annual commitment. That is a real gap in the middle of the market that AWS has just filled.

What the actual data says about volumetric attacks

The best public evidence about whether the free tier of DDoS protection is adequate comes from AWS itself.

In its Threat Landscape Report for Q1 2020 (announced on the AWS Security Blog), AWS reported:

In Q1 2020, a known UDP reflection vector, CLDAP reflection, was observed with a previously unseen volume of 2.3 Tbps. This is approximately 44% larger than any network volumetric event previously detected on AWS. CLDAP reflection attacks of this magnitude caused 3 days of elevated threat during a single week in February 2020 before subsiding.

The report also states that “for any network volumetric event that AWS Shield detected as a DDoS attack, a mitigation was automatically placed,” across 310,954 events that quarter.

Two things follow, and only two — I want to be careful here, because this incident gets over-claimed constantly.

What it does support. AWS’s edge network absorbed a 2.3 Tbps volumetric flood as a matter of routine operation. And the documented scope of Shield Standard is not a token gesture: all customers get it at no additional charge, and Route 53 hosted zones, CloudFront distributions and Global Accelerator standard accelerators “receive comprehensive availability protection against all known network and transport layer attacks.” If your entire public surface is Route 53 plus CloudFront — as this site’s is — then the layer 3/4 volumetric threat is handled, free, by default, with no configuration.

What it does not support. The report describes what AWS Shield as a whole mitigated; it is not a claim that Shield Standard specifically defended one named customer. Shield Standard covers layers 3 and 4 — it is not an application-layer control, and it does not credit your bill. A layer-7 request flood of well-formed HTTP GETs is a WAF problem, not a Shield Standard problem.

And the number in that report that should actually change your architecture is not the headline one. It is this: the 99th-percentile event in Q1 2020 was 43 Gbps.

Ninety-nine percent of the attacks AWS saw were under 43 Gbps. The 2.3 Tbps event was one week, one quarter, worldwide. If you are designing a portfolio’s defences around the tail of that distribution, you have chosen the least likely threat in a dataset of three hundred thousand events, and you are paying monthly for the privilege.

The summary table

TierTrafficComponentsEst. monthly costExposure from omitting the next layer
1 — Portfolio< 10 k visitsRoute 53 + CloudFront + private S3 (OAC), Shield Standard, security headers, tight cache policy≈ $0.55, or $0.00 on the CloudFront Free planNo layer-7 filtering. Acceptable: there is no application to exploit, and scans 404 against a static bucket.
2 — Small business10 k – 1 M visitsTier 1 + AWS WAF (rate-based + 2 managed rule groups), AWS Budgets, CloudWatch alarms$8 – $15 pay-as-you-go, or $15 Pro planNo origin-request collapsing and no bill guarantee. Acceptable while the origin is cheap; a sustained L7 flood is billed to you.
3 — Production at scale> 1 M visits or a real SLATier 2 + Origin Shield if the origin is expensive, richer managed rules, real observability$200+; Business plan is $200 flatNo SRT, no automatic L7 mitigation, no DDoS cost protection credits.
4 — Named targetFintech, government, high-valueTier 3 + Shield Advanced$3,000/month, 1-year commitmentNothing above this. This is the ceiling of what AWS sells.

Read it downward and it looks like a ladder you climb. Read it as a cost function and it is something better: at each step, the thing you are buying is only worth its price if the thing it protects is worth more.

Proportionality is the whole argument

The instinct behind “more layers is always safer” is a good instinct applied without a denominator. Every layer has a monthly price, a configuration surface that can break the site, and an operational cost in the attention it demands. A WAF nobody tunes is a WAF that will eventually block a customer and be disabled in a hurry at 2am. Origin Shield in front of an S3 bucket is a line item that is arithmetically incapable of paying for itself. Shield Advanced on a portfolio is $36,000 of insurance on a $6.60 asset.

What I would keep from this whole exercise is a short ordering. Does the control actually stop something in my threat model — not the reference architecture’s? If yes, can I operate it, or will it rot into a permanent exception? And only then: what does it cost against what it protects?

Run that in that order and the tier-1 answer stops looking like negligence. A private origin, one distribution allowed to read it, real security headers, a free DDoS layer that has demonstrably absorbed 2.3 Tbps, and a bill dominated by a fifty-cent hosted zone is not an under-secured architecture. It is a correctly-sized one — and being able to explain why the other boxes are absent is a stronger position than having drawn them.

All prices verified against AWS’s published pricing and documentation pages in August 2026, and linked inline. AWS pricing and free-tier terms change — the CloudFront flat-rate plans in this post are less than a year old — so check the AWS Pricing Calculator against your own Region and traffic before you commit.