Very Low-Cost Website with Hugo + S3 + CloudFront + Cloudflare
10 min read

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:#000

The 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:#000

The 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:

AspectDetail
Storage costUSD 0.023 per GB/month (ap-southeast-1)
GET request costUSD 0.0004 per 1,000 requests
Downtime99.99% availability SLA
MaintenanceZero — fully managed
ScalingAutomatic, 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:#000

CloudFront 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:#000

Cloudflare 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:#000

Realistic Cost Estimate

ComponentCost ModelEstimate (medium blog/company profile)
S3 StoragePer GB/month< USD 1/month (for hundreds of articles)
S3 GET RequestsPer 1,000 requests< USD 1/month
CloudFrontPer request + transferUSD 1–5/month
CloudflareFree tierUSD 0
Hugo ServiceOnly when publishingAlmost USD 0 (non-permanent container)
Total estimateUSD 2–7/month

Compare this with a traditional WordPress or CMS architecture on a VM:

ApproachEstimate/MonthIdle Cost
EC2 t3.small + RDSUSD 50–100Paid 24/7
EC2 + Managed WordPress hostingUSD 20–50Paid 24/7
This Hugo + S3 + CDN architectureUSD 2–7Almost 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:#000

SEO and AdSense Implications

AspectImpact 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
CrawlabilityOptimal — Googlebot reads HTML directly without needing JS rendering
IndexingFast — no SPA rendering to wait for
AdSense approvalEasier — pure HTML content, no dynamic loading
JSON-LD / Structured DataEasy 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:#000

For 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:

FeaturePossible?Solution
SearchLimitedAlgolia / Pagefind (client-side search)
CommentsNot nativeDisqus, Utterances (external services)
Forms / ContactNot nativeFormspree, Netlify Forms, or Lambda
Login / AuthNot nativeNeeds a separate service
Per-user contentNoNeeds a separate SPA + API
Real-time updatesNoNeeds 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-offImpactMitigation
No native dynamic featuresSearch, comments need external servicesAlgolia, Utterances, Formspree
Build time for large sitesPublishing can take a few minutesHugo incremental builds
Admin panel needs building/choosingMore initial effortHeadless CMS or Git-based editing
Preview before publish is more complexEditors can’t preview directlyHugo 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:#000

It’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.

Portfolio