Very Low-Cost Website with Hugo + S3 + CloudFront + Cloudflare
Many companies still build company profile websites or blogs on a stack that’s far too heavy: an always-on application server, a database, an autoscaling group, a load balancer, and an ops team on standby. For a website whose content rarely changes and needs no real-time interaction, all those layers deliver no value — only cost.
A more sensible approach is moving all the complexity to build time, not runtime. That means the server is only involved when new content is published — not every time a user opens a page. The stack that embodies this: Hugo → S3 → CloudFront → Cloudflare. This architecture has proven stable for technical blogs, company profiles, documentation, and even content-based news portals.
The Big Picture Architecture
graph TB
subgraph "Content Creation Layer (Internal Only)"
Editor["Admin / Editor"] --> AdminUI["Admin Panel<br/>(Internal tool)"]
AdminUI -->|"Save / Publish"| HugoSvc["Hugo Service<br/>(Build engine)"]
end
subgraph "Build & Deploy Pipeline"
HugoSvc -->|"hugo build"| Static["Static Files<br/>(HTML, CSS, JS)"]
Static -->|"aws s3 sync"| S3["Amazon S3<br/>(Object Storage)"]
Static -->|"Invalidate cache"| CFront["CloudFront<br/>(AWS CDN)"]
end
subgraph "Edge & Security Layer"
CF["Cloudflare<br/>DNS, WAF, DDoS Protection,<br/>Edge Cache"]
end
subgraph "User Access (No Server Involved)"
User["Visitor / User"] --> CF
CF -->|"Cache hit"| User
CF -->|"Cache miss"| CFront
CFront -->|"Cache hit"| User
CFront -->|"Cache miss"| S3
S3 --> CFront
end
style Editor fill:#f5f5f4,stroke:#78716c,color:#000
style AdminUI fill:#f5f5f4,stroke:#78716c,color:#000
style HugoSvc fill:#fef3c7,stroke:#d97706,color:#000
style Static fill:#fef3c7,stroke:#d97706,color:#000
style S3 fill:#bfdbfe,stroke:#2563eb,color:#000
style CFront fill:#bfdbfe,stroke:#2563eb,color:#000
style CF fill:#fef3c7,stroke:#d97706,color:#000
style User fill:#f5f5f4,stroke:#78716c,color:#000The key is at the bottom of the diagram: no server is involved when a user accesses a page. Everything the user sees is already a static file cached at the nearest edge node — whether at Cloudflare or CloudFront. S3 is only touched on a cache miss, and even that is very rare for rarely-changing content.
The Role of Each Component
Admin Panel and Hugo Service — Internal Only
This layer is the only dynamic part, and it’s only used by the internal team (admin, editors, content creators). It’s never exposed to the public.
graph LR
subgraph "Content Publishing Flow"
Write["Editor writes<br/>in Admin Panel"]
Save["Clicks Publish"]
Trigger["Admin Panel triggers<br/>Hugo Service"]
Build["Hugo Service runs<br/>hugo build"]
Upload["Upload to S3<br/>Invalidate CloudFront"]
Live["Content live<br/>on all edges"]
end
Write --> Save --> Trigger --> Build --> Upload --> Live
style Write fill:#f5f5f4,stroke:#78716c,color:#000
style Save fill:#f5f5f4,stroke:#78716c,color:#000
style Trigger fill:#fef3c7,stroke:#d97706,color:#000
style Build fill:#fef3c7,stroke:#d97706,color:#000
style Upload fill:#bfdbfe,stroke:#2563eb,color:#000
style Live fill:#bbf7d0,stroke:#16a34a,color:#000The Hugo Service doesn’t run as a manual tool — it’s a service triggered programmatically: via an API call from the admin panel, a message queue, or a webhook from a CMS. It receives a “regenerate” instruction, runs hugo build, and produces the latest static files. After that, its job is done and it doesn’t need to stand by.
Because it isn’t publicly accessible and only active when new content exists, the Hugo Service doesn’t need high availability, doesn’t need auto-scaling, and can run as a small non-permanent container.
Amazon S3 — Static File Storage
S3 is where the Hugo build output is stored. From S3’s perspective, there’s no difference between storing a blog article and storing an image file — they’re all just objects that can be served over HTTP.
What makes S3 attractive for this use case:
| Aspect | Detail |
|---|---|
| Storage cost | USD 0.023 per GB/month (ap-southeast-1) |
| GET request cost | USD 0.0004 per 1,000 requests |
| Downtime | 99.99% availability SLA |
| Maintenance | Zero — fully managed |
| Scaling | Automatic, no configuration needed |
S3 doesn’t know what an “article” or “page” is — it just serves files. And precisely because of that, S3 is very cheap and very stable.
Origin Access Control (OAC) ensures the S3 bucket never needs to be made public — only CloudFront may access it. Users never interact with S3 directly.
CloudFront — Native AWS CDN
CloudFront is the CDN layer sitting between S3 and Cloudflare. Its role is distributing static files to AWS edge locations spread worldwide, so users get content from the geographically nearest server.
graph LR
S3["S3 Origin<br/>(ap-southeast-1)"]
subgraph "CloudFront Edge Locations"
SEA["Singapore PoP"]
JP["Tokyo PoP"]
US["Virginia PoP"]
EU["Frankfurt PoP"]
end
S3 --> SEA & JP & US & EU
UserSEA["User in Indonesia"] --> SEA
UserJP["User in Japan"] --> JP
UserUS["User in America"] --> US
UserEU["User in Europe"] --> EU
style S3 fill:#bfdbfe,stroke:#2563eb,color:#000
style SEA fill:#bbf7d0,stroke:#16a34a,color:#000
style JP fill:#bbf7d0,stroke:#16a34a,color:#000
style US fill:#bbf7d0,stroke:#16a34a,color:#000
style EU fill:#bbf7d0,stroke:#16a34a,color:#000CloudFront also handles TLS termination (HTTPS), per-path cache behavior (for example: cache HTML for 5 minutes, cache JS/CSS for 1 year because file names contain content hashes), and IAM integration with S3 via Origin Access Control.
Cloudflare — the Shield Layer at the Very Front
Cloudflare sits in front of CloudFront — not replacing it. This often looks redundant, but both have different, complementary roles.
graph LR
subgraph "Cloudflare (Shield)"
DNS["DNS Resolution"]
WAF["WAF<br/>(Web Application Firewall)"]
DDoS["DDoS Protection"]
Bot["Bot Management"]
EdgeCache["Edge Cache<br/>(Free global CDN)"]
end
subgraph "CloudFront (AWS Native)"
OAC["Origin Access Control"]
CacheBehavior["Per-path cache behavior"]
TLS["TLS termination"]
Compression["Gzip / Brotli compression"]
end
Internet["Internet Traffic"] --> DNS --> WAF --> DDoS --> Bot --> EdgeCache
EdgeCache -->|"Cache miss"| CacheBehavior --> OAC --> TLS --> S3["S3"]
style Internet fill:#f5f5f4,stroke:#78716c,color:#000
style DNS fill:#fef3c7,stroke:#d97706,color:#000
style WAF fill:#fef3c7,stroke:#d97706,color:#000
style DDoS fill:#fef3c7,stroke:#d97706,color:#000
style Bot fill:#fef3c7,stroke:#d97706,color:#000
style EdgeCache fill:#fef3c7,stroke:#d97706,color:#000
style OAC fill:#bfdbfe,stroke:#2563eb,color:#000
style CacheBehavior fill:#bfdbfe,stroke:#2563eb,color:#000
style TLS fill:#bfdbfe,stroke:#2563eb,color:#000
style Compression fill:#bfdbfe,stroke:#2563eb,color:#000
style S3 fill:#bfdbfe,stroke:#2563eb,color:#000Cloudflare filters dangerous traffic before it reaches AWS infrastructure. Traffic that passes Cloudflare but is in its cache never reaches CloudFront — directly reducing AWS cost, since CloudFront bills by request count and data transfer. On Cloudflare’s free tier, all DDoS protection, basic WAF, and the global CDN are available at no cost.
Why This Architecture Is So Cheap
Nothing to Pay When There Are No Users
This is the fundamental difference from server-based architectures. In traditional architecture, servers keep running and keep being paid regardless of whether visitors exist. In this static architecture, no component consumes cost when there are no requests.
graph LR
subgraph "❌ Traditional Architecture"
ServerAlways["EC2 / VM<br/>Always on<br/>Always paid"]
DB["Database<br/>Always on<br/>Always paid"]
LB["Load Balancer<br/>Always active<br/>Min USD 16/month"]
end
subgraph "✅ Static Architecture"
S3Cost["S3<br/>Pay per GB of storage<br/>+ per GET request"]
CFCost["CloudFront<br/>Pay per request<br/>+ per GB transferred"]
CFlareCost["Cloudflare<br/>Free on the free tier"]
end
style ServerAlways fill:#fecaca,stroke:#dc2626,color:#000
style DB fill:#fecaca,stroke:#dc2626,color:#000
style LB fill:#fecaca,stroke:#dc2626,color:#000
style S3Cost fill:#bbf7d0,stroke:#16a34a,color:#000
style CFCost fill:#bbf7d0,stroke:#16a34a,color:#000
style CFlareCost fill:#bbf7d0,stroke:#16a34a,color:#000Realistic Cost Estimate
| Component | Cost Model | Estimate (medium blog/company profile) |
|---|---|---|
| S3 Storage | Per GB/month | < USD 1/month (for hundreds of articles) |
| S3 GET Requests | Per 1,000 requests | < USD 1/month |
| CloudFront | Per request + transfer | USD 1–5/month |
| Cloudflare | Free tier | USD 0 |
| Hugo Service | Only when publishing | Almost USD 0 (non-permanent container) |
| Total estimate | — | USD 2–7/month |
Compare this with a traditional WordPress or CMS architecture on a VM:
| Approach | Estimate/Month | Idle Cost |
|---|---|---|
| EC2 t3.small + RDS | USD 50–100 | Paid 24/7 |
| EC2 + Managed WordPress hosting | USD 20–50 | Paid 24/7 |
| This Hugo + S3 + CDN architecture | USD 2–7 | Almost zero |
For high-traffic websites (millions of page views per month), CloudFront and bandwidth costs will rise — but still far cheaper than scaling EC2 + a database for the same traffic.
Performance and SEO — Built-in Advantages
Static HTML served from a CDN edge isn’t just cheap — it’s also the fastest way to serve a web page. No database queries, no server-side rendering, no cold starts. The files are ready, just send them.
graph LR
subgraph "Server-Side Rendering (Traditional)"
Req1["Request"] --> Server1["Server"]
Server1 --> DB1["Query DB"]
DB1 --> Render1["Render template"]
Render1 --> Resp1["HTML response"]
Latency1["Latency: 200ms - 2s+"]
end
subgraph "Static Site from CDN Edge"
Req2["Request"] --> Edge["CDN Edge<br/>(geographically nearest)"]
Edge --> Resp2["HTML response<br/>(from cache)"]
Latency2["Latency: 10-50ms"]
end
style Server1 fill:#fecaca,stroke:#dc2626,color:#000
style DB1 fill:#fecaca,stroke:#dc2626,color:#000
style Latency1 fill:#fecaca,stroke:#dc2626,color:#000
style Edge fill:#bbf7d0,stroke:#16a34a,color:#000
style Latency2 fill:#bbf7d0,stroke:#16a34a,color:#000SEO and AdSense Implications
| Aspect | Impact on Static Sites |
|---|---|
| Core Web Vitals (LCP, FID, CLS) | Excellent — HTML is pre-rendered, no layout shift from SSR |
| Time to First Byte (TTFB) | Very low — CDN edge response |
| Crawlability | Optimal — Googlebot reads HTML directly without needing JS rendering |
| Indexing | Fast — no SPA rendering to wait for |
| AdSense approval | Easier — pure HTML content, no dynamic loading |
| JSON-LD / Structured Data | Easy to inject in Hugo templates |
Build and Deploy Flow
Two Modes to Handle
There are two different scenarios that need to be prepared differently:
graph TB
subgraph "Mode 1: Publishing New Content"
E1["Editor clicks Publish"] --> T1["Admin Panel triggers Hugo Service"]
T1 --> B1["Hugo build (incremental or full)"]
B1 --> U1["Sync to S3 (only changed files)"]
U1 --> I1["Invalidate CloudFront cache<br/>(specific changed paths)"]
I1 --> Live1["Content live within 1-2 minutes"]
end
subgraph "Mode 2: Updating Templates / Theme"
D1["Developer pushes to repo"] --> CI1["CI/CD Pipeline"]
CI1 --> B2["Full hugo build"]
B2 --> U2["Full sync to S3"]
U2 --> I2["Invalidate all CloudFront cache"]
I2 --> Live2["Changes live"]
end
style E1 fill:#f5f5f4,stroke:#78716c,color:#000
style T1 fill:#fef3c7,stroke:#d97706,color:#000
style B1 fill:#fef3c7,stroke:#d97706,color:#000
style U1 fill:#bfdbfe,stroke:#2563eb,color:#000
style I1 fill:#bfdbfe,stroke:#2563eb,color:#000
style Live1 fill:#bbf7d0,stroke:#16a34a,color:#000
style D1 fill:#f5f5f4,stroke:#78716c,color:#000
style CI1 fill:#f5f5f4,stroke:#78716c,color:#000
style B2 fill:#fef3c7,stroke:#d97706,color:#000
style U2 fill:#bfdbfe,stroke:#2563eb,color:#000
style I2 fill:#bfdbfe,stroke:#2563eb,color:#000
style Live2 fill:#bbf7d0,stroke:#16a34a,color:#000For publishing new content, CloudFront invalidation should ideally only target changed paths (for example: /artikel/new-title/* and /index.html) — not invalidating the whole cache. This is cheaper and faster because other content’s cache doesn’t need refreshing.
Trade-offs and Limitations
This architecture is a very appropriate choice for suitable use cases, but there are real limitations to understand.
No Dynamic Features at Runtime
Everything produced is static HTML. Features requiring per-request server-side logic can’t be implemented natively:
| Feature | Possible? | Solution |
|---|---|---|
| Search | Limited | Algolia / Pagefind (client-side search) |
| Comments | Not native | Disqus, Utterances (external services) |
| Forms / Contact | Not native | Formspree, Netlify Forms, or Lambda |
| Login / Auth | Not native | Needs a separate service |
| Per-user content | No | Needs a separate SPA + API |
| Real-time updates | No | Needs separate WebSocket / SSE |
Build Time Grows with Content Volume
Hugo is very fast — thousands of pages in seconds. But for websites with hundreds of thousands of articles, build time starts to be felt. The solution is incremental builds: Hugo only re-renders pages whose content changed, not the whole site.
Admin Panel and Hugo Service Complexity
Unlike WordPress, which includes a CMS, this architecture requires an admin panel built or chosen separately. It can be a headless CMS (Contentful, Sanity, Forestry), a custom admin panel, or even direct Markdown editing in Git. Each choice has its own trade-offs.
| Trade-off | Impact | Mitigation |
|---|---|---|
| No native dynamic features | Search, comments need external services | Algolia, Utterances, Formspree |
| Build time for large sites | Publishing can take a few minutes | Hugo incremental builds |
| Admin panel needs building/choosing | More initial effort | Headless CMS or Git-based editing |
| Preview before publish is more complex | Editors can’t preview directly | Hugo server mode + staging environment |
Which Use Cases This Fits
graph LR
subgraph "Highly Suitable"
G1["Company profiles"]
G2["Engineering / marketing blogs"]
G3["Documentation sites"]
G4["Article-based news portals"]
G5["Campaign landing pages"]
G6["Portfolio / personal sites"]
end
subgraph "Needs Evaluation"
M1["Websites with complex search<br/>(possible with Algolia)"]
M2["Multilingual with different content<br/>(Hugo supports i18n)"]
M3["Very frequently updated content<br/>(build time becomes a consideration)"]
end
subgraph "Not Suitable"
B1["Applications with per-user login"]
B2["Dashboards with real-time data"]
B3["E-commerce with cart / checkout"]
B4["Interactive forums / communities"]
end
style G1 fill:#bbf7d0,stroke:#16a34a,color:#000
style G2 fill:#bbf7d0,stroke:#16a34a,color:#000
style G3 fill:#bbf7d0,stroke:#16a34a,color:#000
style G4 fill:#bbf7d0,stroke:#16a34a,color:#000
style G5 fill:#bbf7d0,stroke:#16a34a,color:#000
style G6 fill:#bbf7d0,stroke:#16a34a,color:#000
style M1 fill:#fef3c7,stroke:#d97706,color:#000
style M2 fill:#fef3c7,stroke:#d97706,color:#000
style M3 fill:#fef3c7,stroke:#d97706,color:#000
style B1 fill:#fecaca,stroke:#dc2626,color:#000
style B2 fill:#fecaca,stroke:#dc2626,color:#000
style B3 fill:#fecaca,stroke:#dc2626,color:#000
style B4 fill:#fecaca,stroke:#dc2626,color:#000It’s worth noting: even if the main website needs dynamic features (login, dashboards), static content can still be separated. Engineering blogs, public documentation, or marketing pages can use this architecture independently, while dynamic parts use a different stack.
Summary
- Core principle: move all the complexity to build time, not runtime. The server is only involved when new content is published — not when a user accesses a page.
- Stack: Hugo (build engine) → S3 (object storage) → CloudFront (AWS CDN) → Cloudflare (shield layer). No VMs, no database, no runtime server.
- The Hugo Service is non-permanent: only active when a publish trigger arrives, can run as a small container, no high availability needed.
- Cloudflare + CloudFront isn’t redundant: Cloudflare acts as the shield (WAF, DDoS, DNS, edge cache) before traffic reaches AWS; CloudFront is the native AWS CDN with Origin Access Control to S3.
- Cost estimate: USD 2–7/month for a medium blog or company profile — compared to USD 50–100+/month for a traditional VM + database stack.
- Performance: static HTML from a CDN edge has 10–50ms TTFB, excellent Core Web Vitals, optimal crawlability for SEO, and is safe for AdSense.
- Real limitations: no native dynamic features (search needs Algolia, comments need external services, no per-user login). This isn’t a drawback for use cases that are genuinely content-static.
- Best for: company profiles, blogs, documentation, article news portals. Not suitable for applications with login, real-time data, or complex user interaction.