Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 050 / 100

Nuxt on AWS: what every Vue & Nuxt developer should know

Run Nuxt in the AWS ecosystem.

Expona Admin6 min read

Nuxt on AWS: what every Vue & Nuxt developer should know. Run Nuxt in the AWS ecosystem.

Introduction

Running Nuxt applications on AWS can feel like navigating a maze of options—Lambda, ECS, S3, CloudFront—each with different trade-offs for cost, complexity, and performance. We've deployed enough Nuxt apps across AWS to know that choosing the right service for your workload is one of the highest-impact decisions you'll make early on. In this post, we'll walk through the main AWS deployment patterns for Nuxt, from serverless functions to containerized servers and static hosting, so you can make decisions backed by production experience rather than marketing copy.

Lambda Preset for Serverless Deployments

Nuxt's Lambda preset abstracts away much of the plumbing needed to run your app on AWS Lambda. It handles the mismatch between Lambda's event-driven model and Nuxt's server request/response cycle by wrapping your Nitro server in a Lambda handler. For teams building smaller to medium apps or those with unpredictable traffic patterns, this is often the most cost-effective route.

To get started, install the preset and configure your nuxt.config.ts:

typescript
export default defineNuxtConfig({
  nitro: {
    presets: 'aws_lambda'
  }
})

When you run nuxi build, Nitro generates a .output folder containing a Lambda-compatible handler. You can then package this with a tool like Serverless Framework or AWS SAM (Serverless Application Model) and deploy to Lambda behind API Gateway. API Gateway handles routing HTTP requests to your Lambda function and returning responses.

The real advantage appears in the billing: you pay only for the CPU time your function actually runs, not for idle servers. A Nuxt app handling 10 requests per day costs almost nothing on Lambda. However, there are limits to keep in mind. Lambda functions have a 15-minute timeout (or 900 seconds), so long-running server operations or batch jobs don't belong here. Cold starts can add 1–3 seconds to requests on infrequently invoked functions, which may degrade user experience if your site isn't getting steady traffic.

We've had good results pairing Lambda with Provisioned Concurrency for apps that need consistent sub-second response times. It's a small monthly cost per concurrent execution, but it eliminates cold starts entirely.

ECS for Long-Running Servers

When you need stateful connections, background jobs that run longer than a few minutes, or predictable, sustained traffic, ECS (Elastic Container Service) is your friend. ECS lets you run Docker containers on either EC2 instances you manage or on Fargate (where AWS handles the infrastructure).

A typical ECS setup for Nuxt involves building a Docker image with your Nuxt app, pushing it to ECR (Elastic Container Registry), and defining an ECS task that runs that image. Here's a minimal Dockerfile:

typescript
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY .output ./.output
EXPOSE 3000
CMD ["node", "./.output/server/index.mjs"]

You'll build and push this to ECR, then create an ECS task definition that references the image. Deploy it behind an Application Load Balancer (ALB) for automatic scaling and health checks. The ALB forwards traffic to your running Nuxt containers, and ECS can spin up or down instances based on CPU or memory metrics.

Fargate removes the need to provision and manage EC2 instances, which is simpler but costs more per hour than reserved EC2 capacity. For steady-state workloads with predictable load, EC2 with ECS is often cheaper. For variable or bursty traffic, Fargate's auto-scaling is more flexible.

The downside of ECS is overhead. You're responsible for logging, monitoring, container security, and infrastructure updates. In exchange, you get full control over memory, CPU, and networking. This is the right choice when Lambda's constraints don't fit your needs.

S3 and CloudFront for Static Output

If your Nuxt app is mostly static—think a documentation site, landing page, or marketing portal with infrequent updates—deploying to S3 backed by CloudFront offers simplicity and exceptional performance at a fraction of the cost of running servers.

Generate a static build with nuxi generate, which pre-renders all your routes to HTML files. Upload the .output/public folder to an S3 bucket, then create a CloudFront distribution pointing to that bucket as its origin. CloudFront caches your content at edge locations worldwide, so users download from a server near them.

The configuration is straightforward. In your nuxt.config.ts, ensure you're using the static preset if needed:

typescript
export default defineNuxtConfig({
  nitro: {
    presets: 'aws'
  },
  routeRules: {
    '/': { prerender: true },
    '/about/**': { prerender: true },
    '/blog/**': { swr: 3600 }
  }
})

The routeRules object lets you control which routes are pre-rendered at build time and which are generated on-demand. The swr: 3600 (stale-while-revalidate) option tells the server to serve cached HTML for an hour, then regenerate it in the background.

CloudFront distributions cost roughly $0.085 per GB transferred (prices vary by region), plus a small monthly fee for the distribution itself. For a site serving 1TB of traffic monthly, expect around $100 in transfer costs. Compare that to running even a small EC2 instance, which costs $5–$15 per month, and the static path is hard to beat for appropriate workloads.

Cache Headers on Assets

Whether you're using Lambda, ECS, or S3+CloudFront, controlling cache headers is essential to performance and cost. Browsers and CDNs respect the Cache-Control header, so incorrect settings waste bandwidth and slow down users.

Nuxt's build process generates hashed filenames for assets (JavaScript, CSS, images) by default. A file like app.a1b2c3d4.js includes a hash that changes only when the file's content changes. This lets you set aggressive caching on these hashed assets: they won't change unless you redeploy.

Configure caching in your Nitro config to serve different headers for different paths:

typescript
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/_nuxt/**': { cache: { maxAge: 31536000 } }, // 1 year
      '/**': { cache: { maxAge: 3600, sMaxAge: 3600 } } // 1 hour
    }
  }
})

The _nuxt folder contains your hashed assets, so a one-year max-age is safe. Other routes (like your HTML pages) get one hour, so updates propagate within an hour of deployment.

If you're using CloudFront, these headers automatically propagate to the edge. If you're on Lambda with API Gateway, the headers are passed through. If you're on S3, you can set cache headers when uploading objects or use CloudFront's cache behaviors to override them at the CDN layer.

A common mistake is setting the same cache TTL for both hashed and unhashed assets. Hashed assets are immutable—they'll never change—so cache them aggressively. Unhashed assets (your HTML) must have shorter TTLs so users get updates relatively quickly.

Common Mistakes

  • Mixing cache strategies: Setting Cache-Control: max-age=31536000 on your HTML file means users see old versions for a year. Use short TTLs (1 hour) for HTML and long TTLs (1 year or more) for hashed assets only.
  • Ignoring Lambda timeouts: Building a Nuxt app that makes external API calls during server-side rendering can hit Lambda's 15-minute limit. Test your server-render time locally and use timeout values in fetch calls.
  • Over-provisioning ECS: It's tempting to allocate lots of CPU and memory to ECS tasks, but auto-scaling based on actual metrics is more cost-effective. Start small and scale up if metrics warrant it.
  • Forgetting CloudFront invalidation: After deploying new code to S3, CloudFront may still serve old cached files. Include a cache invalidation step in your CI/CD pipeline, or accept a delay until the old cache expires.
  • Not monitoring cold starts: If you choose Lambda, monitor cold start latency in production. If it's unacceptable, move to Provisioned Concurrency or switch to ECS.

Key Takeaways

  • Lambda preset suits unpredictable or light traffic; it scales to zero and costs nothing when idle, but has a 15-minute timeout and cold start latency.
  • ECS on Fargate or EC2 is best for stateful, long-running, or CPU-intensive workloads that need sustained performance.
  • S3 + CloudFront is the most cost-effective for mostly static content, offering global performance at minimal operational overhead.
  • Always use hashed filenames and set aggressive cache headers on immutable assets; use short TTLs on HTML.
  • Test your chosen deployment pattern locally and in a staging environment to catch timeout, memory, or scaling issues before production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles