Case Study: Static Marketing Website
Requirements
- Serve static HTML, CSS, JavaScript and images over HTTPS on a custom domain.
- Fast for visitors in Europe, North America and Asia — this is a marketing site and bounce rate is the metric.
- Survive a traffic spike of 50x with no intervention, because the spikes are the entire point of marketing.
- A content editor can publish a change without an engineer.
- Cost close to nothing when nobody is visiting.
Deliberately not requirements
Half of a design is what it refuses to do. These are the refusals.
- No per-user content, no login, no session state — every visitor gets identical bytes.
- No server-side rendering, no database, no API.
- No sub-second global failover; a few minutes of stale content after a publish is entirely acceptable.
How the design got here
In order. Each stage leads with the problem that forced it.
Object storage behind a DNS name
The requirement itself, and nothing more. The workload is "return the same bytes to everyone". Object storage returns bytes over HTTP, scales horizontally without any action from you, and charges per gigabyte stored and transferred. There is no compute tier here because no computation happens — starting from a web server would be starting from a habit instead of from the workload.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Object storage, not a web server on a VM. | There is no request-time computation, so there is nothing for a server to do except read a file and hand it back — which is what object storage already is, with elasticity and durability built in and no patching. | A small VM running nginx, which is genuinely simpler to reason about if you already run VMs and already have a patching pipeline. | You give up per-request logic entirely: no redirects beyond what the storage layer supports, no header manipulation, no server-side includes. The moment you need one of those, you are gluing something on rather than configuring it. |
| The bucket is public, and that is the design — not a finding. | Public exposure is only a problem when the content is not meant to be public (§103). Marketing HTML is meant to be read by anyone with a browser; a policy that makes it world-readable is expressing intent, not leaking. | Keep the bucket private and require signed URLs, which is correct for user uploads and absurd for a homepage. | You must be disciplined about what else goes in that bucket. The failure mode is not the public policy — it is the day someone drops a spreadsheet of leads next to the images. |
| Deploy by syncing the generator's output directory. | The whole site is a directory of files, so a publish is a copy. There is no build to orchestrate at runtime and nothing to restart. | A CMS with a database and a rendering tier, which is the right answer when non-technical editors need previews, workflow and scheduling. | Editors need a path that is not "learn git", which is exactly the problem that forces the third stage. |
A CDN in front of the bucket
Two concrete problems on the same week. First, HTTPS on the custom domain: the storage endpoint serves TLS for its own hostname, not for example.com, and the marketing team needs the real domain in every link. Second, latency — the bucket lives in one European region, and a synthetic check from Singapore measured a first-byte time roughly six times worse than one from Frankfurt, on a site whose only KPI is bounce rate.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| The CDN terminates TLS and owns the certificate lifecycle. | A certificate that renews automatically at the edge removes the single most common cause of a total outage on a site like this: an expiry date nobody was watching. | A certificate you issue and install yourself, which you should only choose if you have a reason the managed one cannot satisfy. | You depend on the CDN's issuance and validation path, and DNS validation records must stay in place forever. Deleting a "mystery" DNS record is how this breaks a year later. |
| Lock the origin bucket to the CDN and make it private. | With two public entry points, half your traffic can bypass the cache, the logs and any header policy you set. One front door is a simpler thing to reason about and a smaller thing to defend. | Leave the bucket public as well, which costs nothing and is fine right up until someone links the origin URL directly. | An origin access configuration to maintain, and one more thing that can be misconfigured into a 403 on every page after an otherwise innocent policy edit. |
| Long cache lifetimes on hashed assets, short on HTML. | Fingerprinted CSS and JS filenames change whenever their content changes, so they can be cached effectively forever. HTML must be re-fetched or a publish takes a day to appear. | Cache everything briefly, which is simpler and gives up most of the benefit you just paid for. | The build must actually fingerprint assets, and someone has to remember that a stale HTML page is a caching decision rather than a bug. |
| Accept the CDN as a genuine dependency, with a single region behind it. | Object storage in one region is already replicated across zones by the provider, and the CDN keeps serving cached content through a brief origin problem. That is enough availability for a marketing site. | Replicate the bucket to a second region with origin failover, which is a real option and is not warranted here. | A regional storage outage that outlasts your cache means a down site. For this workload that is an acceptable, documented risk — and saying so out loud is better engineering than quietly building for it. |
Publish from git, with invalidation
A marketing manager dragged files into the storage console, overwrote the stylesheet with an older copy, and the site was visibly broken for six hours because the CDN had happily cached it. Nobody could say what changed or roll it back, because the only history was the bucket's current contents.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| The bucket is written only by CI, never by a human. | It makes git the source of truth, which gives you history, review, blame and a one-command rollback — all of which you already have and were not using. | Keep console access for emergencies, which sounds prudent and reliably becomes the normal path within a month. | Publishing now depends on CI being available. That is a real dependency, and the honest mitigation is a documented break-glass procedure rather than a permanently open door. |
| The deploy identity is a short-lived role scoped to this bucket and this distribution. | A static-site pipeline needs to write objects and invalidate paths. Nothing else. Scoping the identity to precisely that is cheap here, which is exactly why it should be done here. | A long-lived access key stored as a CI variable, which works and is a credential with no expiry sitting in a system many people can read. | A little more configuration in the CI provider, and an identity federation that must be maintained across CI platform migrations. |
| Invalidate only the paths that changed. | A full invalidation empties the cache globally, so the next visitor everywhere pays origin latency and your origin absorbs a burst it never normally sees. | Invalidate everything on every publish — simpler, and fine on a site that publishes weekly. | The pipeline must know which files changed, which means it must diff rather than blindly sync. Get it wrong and the site is stale in a way that looks like a caching bug. |
What would break this
Every design has a load, a failure or an organization size at which it stops being the right one.
- A contact form. The moment a visitor submits data, you need something that receives it — a function, a form service, an API. That is the first genuine compute in the system and the first thing that can be abused, rate-limited and spammed.
- Anything per-visitor: a logged-in area, a personalized price, a region-specific banner rendered server-side. Static means every visitor gets identical bytes; the first exception ends the architecture rather than bending it.
- Search over the content. A few hundred pages can be searched client-side with a prebuilt index; a few thousand cannot, and you need a search service and something to keep it in sync.
- Editors who need preview, scheduling and approval workflow. That is a CMS, and a CMS is a database and a rendering tier — a different case study, closer to
cs-api-database. - Content that changes many times an hour. The cache lifetimes that make this design fast become the thing fighting you, and a publish-then-invalidate loop stops being cheap.
- A legal or compliance requirement to know who viewed what. CDN access logs answer "how many"; they do not answer "which authenticated person", because there are no authenticated people.
- Until one of those arrives, adding a load balancer, a container platform, an autoscaling group or a Kubernetes cluster to this design buys you nothing and costs you a patching obligation, an on-call rotation and a monthly bill. The correct amount of infrastructure here is the amount already on this page.
Cost shape
Drivers and relative weights. Never a price.
Bars are relative weights, not currency. Real rates depend on provider, region, commitment and volume.