node.js → aws · 2026 deployment & migration playbook

Migrate a Node.js app to AWS — the right compute target, the data layer, and who runs the cutover.

A Node.js app can run on AWS five different ways, and picking the wrong one is the single most expensive decision you will make. This is the senior-engineer playbook for deploying and migrating Node.js to AWS: when a long-running API belongs on App Runner or ECS/Fargate versus Lambda + API Gateway, RDS/Aurora vs DynamoDB, ElastiCache, WebSockets, secrets, Dockerizing, CI/CD, the database cutover, and the gotchas (cold starts, connection pooling with RDS Proxy). A MAP-funded AWS partner can run the whole thing — often at little-to-no cost to you.

compute options
3 sensible
cutover downtime
0–15 min
project length
2–6 weeks
cost to you (MAP)
$0–low
TL;DR
  • The first decision dominates everything else: is your Node.js app a long-running service (a persistent HTTP API, a WebSocket server, a queue worker, anything holding connection pools or in-memory state) or is it event-driven and bursty? Long-running → AWS App Runner (Heroku-like simplicity) or Amazon ECS on Fargate (full control). Event-driven / spiky / cron-like → AWS Lambda behind API Gateway. Most production Express/Fastify/NestJS APIs are long-running and belong on Fargate or App Runner; Lambda shines for spiky, stateless, or glue workloads.
  • The data layer maps cleanly: a relational app (PostgreSQL/MySQL via Prisma, Sequelize, TypeORM, Knex) → Amazon RDS or Aurora; a key-value / high-scale / single-table app → Amazon DynamoDB. Redis (BullMQ, ioredis, sessions, rate-limits) → Amazon ElastiCache. Environment variables → AWS Secrets Manager (secrets) + SSM Parameter Store (config). You Dockerize once (a ~12-line Dockerfile), wire CI/CD to ECR + ECS (or keep GitHub Actions), and migrate the database with AWS DMS so the cutover is a short DNS/connection-string flip.
  • The two gotchas that bite Node teams specifically: Lambda cold starts (a cold container init plus your dependency tree can add 200ms–2s+ to the first request — fatal for latency-sensitive synchronous APIs unless you use provisioned concurrency) and database connection exhaustion (a connection-per-Lambda or autoscaling-Fargate-task pattern opens far more Postgres/MySQL connections than the instance allows — the fix is Amazon RDS Proxy). CloudRoute routes you to a vetted AWS partner who runs it end-to-end; when the workload qualifies for the AWS Migration Acceleration Program (MAP), AWS funds the migration and the customer pays little-to-nothing.
the one decision that matters

ILong-running service or event-driven? The fork that decides the architecture

Almost every bad Node.js-on-AWS architecture traces back to one mistake: forcing a long-running app into a serverless mold (or vice-versa) because someone heard "Lambda is the AWS way to run Node." It is not. The honest first question is about your workload's shape, not the trend.

A long-running service benefits from staying warm and holding state between requests: a persistent Express/Fastify/NestJS HTTP API, a WebSocket server, a worker draining a queue, anything maintaining a database connection pool, an in-memory cache, or a long-lived gRPC/Kafka client. These want a container that boots once and serves many requests — on AWS, AWS App Runner or Amazon ECS on Fargate. The pool is reused, there is no per-request cold-start tax, and the cost model is predictable per running task.

An event-driven workload is the opposite: bursty, stateless, and idle much of the time — image-processing on S3 upload, a webhook receiver, a scheduled job, a Stripe/Twilio callback, a fan-out from SQS/EventBridge, an occasional low-traffic internal API. These map to AWS Lambda — you pay only while code runs, it scales from zero to thousands of concurrent executions automatically, and there is no idle server to pay for. Behind Amazon API Gateway, a Lambda becomes a fully-managed HTTP endpoint.

The trap is the middle. Teams take a steady-traffic Express API doing 50 requests/second, wrap it in `@vendia/serverless-express`, and put it on Lambda — and now they pay a cold-start tax on latency, fight connection-pool exhaustion against RDS, and pay *more* than a small always-on Fargate task would cost at that volume. Lambda is brilliant for spiky and idle; it is a poor fit for steady, latency-sensitive, connection-pooling synchronous APIs. And nothing forces a single answer: mature Node.js systems are frequently *hybrid* — the synchronous core on Fargate, the spiky asynchronous edges (webhooks, image processing, scheduled tasks, fan-out) on Lambda — which is exactly the allocation a migration partner produces first.

the 30-second test

Ask: does a request benefit from a warm process with an open database connection pool? If yes (a typical REST/GraphQL API, a WebSocket server, a worker) → App Runner or ECS/Fargate. If the work is bursty, stateless, idle-heavy, or cron-like (webhooks, S3-triggered processing, scheduled jobs) → Lambda + API Gateway. Steady high-throughput synchronous API on Lambda is the most common over-engineering mistake — and the most common source of cold-start and connection-pool pain.

the three landing spots

IIApp Runner vs ECS/Fargate vs Lambda — picking the Node.js compute target

There are five technical ways to run Node on AWS (App Runner, ECS/Fargate, Lambda, ECS/EC2, EKS), but for a team migrating an existing app there are only three sensible ones. Here is what each is genuinely good at, in plain terms; the full side-by-side is in the comparison table below.

AWS App Runner is the closest thing AWS has to Heroku or Render for Node. Point it at a container image (or a GitHub repo for managed source builds) and it builds, deploys, serves HTTPS on a managed domain, and autoscales on request volume — the fastest path to "my Node API is live on AWS" with the least operational surface, and the right call for one or a handful of stateless web services where you do not want to own a VPC, a load balancer, and task definitions. The tradeoff is less control: VPC egress and private database connectivity are configurable but constrained, and very complex topologies outgrow it.

Amazon ECS on Fargate is the workhorse and the most common landing spot for production Node.js APIs. You get serverless containers (no servers to patch) with full control: VPC networking, security groups, an Application Load Balancer, fine-grained task CPU/memory, per-service autoscaling, and clean separation of an API service from a worker service. Your Express/Fastify/NestJS process boots once per task and serves many requests, so the pool is reused and there is no cold-start tax. This is where teams land when they have a real database, workers, WebSockets, or networking requirements — most teams migrating an established app.

AWS Lambda + Amazon API Gateway is the serverless target: per-millisecond billing, automatic scale from zero, zero idle cost. For Node it is ideal for spiky APIs, webhook handlers, scheduled jobs, S3/DynamoDB-stream triggers, and asynchronous fan-out — you write handler functions (or run a framework via an adapter, with caveats) and API Gateway provides the HTTP front door. The costs are specific: cold starts on latency-sensitive paths, a 15-minute execution ceiling, payload limits, and the database-connection problem at scale (see the gotchas); provisioned concurrency removes cold starts but also the scale-to-zero advantage. (Skip the other two targets when migrating — ECS on EC2 pays off only for specific cost/GPU/placement needs, and EKS is overkill for a single Node app unless you already run Kubernetes.)

default recommendations

App Runner — a few stateless HTTP services, git-push simplicity, minimal ops. ECS/Fargate — production API + workers, a real relational database, WebSockets, or VPC/networking control (the default for "we have a real app"). Lambda + API Gateway — spiky/idle-heavy/event-driven workloads, or the asynchronous edges of a long-running system. Unsure for a steady synchronous API? Choose Fargate — fewest sharp edges for Node.

where your data lands

IIIThe data layer: RDS/Aurora vs DynamoDB, plus ElastiCache for Redis

The compute choice gets the attention; the data layer is where the migration risk actually lives. The good news for Node teams is that the mapping is mechanical and your ORM usually does not change.

If your Node app talks SQL — PostgreSQL or MySQL through Prisma, Drizzle, Sequelize, TypeORM, Knex, or raw `pg`/`mysql2` — it moves to Amazon RDS (the like-for-like managed database) or Amazon Aurora (AWS's cloud-native PostgreSQL/MySQL-compatible engine, with faster failover, autoscaling storage, up to 15 read replicas, and Serverless v2 capacity scaling). Because both speak the same wire protocol as your current database, schema, queries, and ORM models are unchanged — you swap the connection string and migrate the data. A common pattern is RDS first to minimize change at cutover, then evaluate Aurora once stable; Serverless v2 suits spiky workloads because it scales capacity with load.

If your app is key-value or document-shaped and built for horizontal scale — heavy single-table access, very high write throughput, predictable millisecond latency at scale — Amazon DynamoDB is the AWS-native fit, and it pairs especially well with Lambda (it scales the same way functions do, and its HTTP connection model sidesteps connection exhaustion entirely). The honest caveat: DynamoDB is not a drop-in for a relational app. If you rely on joins, ad-hoc queries, and cross-table transactions, "migrate Postgres to DynamoDB" is a *refactor*, not a migration — so most teams keep relational data on RDS/Aurora and reach for DynamoDB only for specific high-scale tables or new services.

For Redis — and Node apps use it constantly via ioredis/node-redis for caching, BullMQ/Bee-Queue job queues, session stores, rate-limiters, and pub/sub — the target is Amazon ElastiCache (Redis OSS or Valkey). It speaks the same protocol, so your client code keeps working with a connection-string change and cache, queues, and rate-limiters move with effectively no rewrite. Two supporting pieces round out the layer: file uploads and user assets belong on Amazon S3 (mandatory, not optional — local-disk writes break the moment you run multiple tasks or invocations), and full-text search on a self-hosted Elasticsearch moves to Amazon OpenSearch Service. Both are easy to forget until something stateful breaks across instances, so both live on the migration checklist.

the real-time question

IVWebSockets and real-time: ALB + Fargate vs API Gateway WebSocket APIs

Real-time is where the long-running-vs-serverless fork gets sharp, because WebSockets are inherently long-lived connections and Lambda is inherently ephemeral. If your Node app uses Socket.IO, `ws`, or GraphQL subscriptions, this section decides your compute choice as much as the API does.

The straightforward path for a Node WebSocket server (Socket.IO, `ws`, `uWebSockets.js`) is to run it as a long-running container on ECS/Fargate behind an Application Load Balancer — the ALB handles WebSocket upgrades natively, holds the persistent connection, and the connection lives in your process exactly as today. This is the lowest-friction move because your real-time code does not change. The one addition at scale: across multiple Fargate tasks, Socket.IO needs a shared adapter (the Redis adapter backed by ElastiCache) so a message published on one task reaches clients connected to another — a well-trodden pattern, not a rewrite.

The serverless path is Amazon API Gateway WebSocket APIs: API Gateway manages the persistent connections and invokes a Lambda on connect/disconnect/message events, and you push messages back out via the API Gateway Management API. It scales to huge connection counts with no servers to run and excels at fan-out-style real-time (notifications, live dashboards) where each message is a discrete event. The tradeoff is a genuinely different programming model — your Socket.IO server does not "just run" here; you re-implement the connection lifecycle around API Gateway routes and a connection store (usually DynamoDB). So the honest recommendation: if you already have a working Socket.IO / `ws` server, keep it long-running on Fargate behind an ALB and add the Redis adapter (the cheapest, lowest-risk move most partners default to); reach for API Gateway WebSocket APIs only for greenfield real-time, zero idle cost, or connection counts large and spiky enough that running your own socket fleet is the bigger cost.

Socket.IO across multiple tasks

On a single instance, Socket.IO "just works." The moment you autoscale to multiple Fargate tasks, a message emitted on task A will not reach a client connected to task B unless you add the Socket.IO Redis adapter backed by ElastiCache (or the AWS adapter). This is the #1 "real-time broke after we scaled" bug in Node migrations — design the shared adapter in from day one, not after the incident.

the hands-on part

VDockerizing, env → Secrets Manager, and CI/CD to ECR + ECS

For a long-running Node app, three mechanical tasks make up most of the hands-on work: writing a container image, moving environment variables to AWS-native secret stores, and wiring a deploy pipeline. None is hard; all are easy to do badly, which is why they live on a checklist.

Dockerizing a Node app is usually a ~12-line Dockerfile, and the detail worth getting right is the multi-stage build: a build stage runs `npm ci` and `npm run build` (for TypeScript/Next/Nest), then a slim runtime stage (`node:22-alpine` or distroless) copies only `node_modules` and the built output and runs as non-root — small image, low attack surface. Set `NODE_ENV=production`, expose the listen port, and make the start command your real entrypoint (`node dist/main.js`), not `npm start`. Build in CI and push to Amazon ECR; App Runner can build from source instead, but a Dockerfile is the portable choice most partners standardize on so the same image runs in Fargate, App Runner, or locally.

Environment variables are the second translation. A Node app reads a flat `process.env` bag — `DATABASE_URL`, `REDIS_URL`, JWT secrets, API keys, feature flags. On AWS you split by sensitivity: real secrets into AWS Secrets Manager (which can auto-rotate RDS credentials), non-sensitive config into SSM Parameter Store. Both inject into the ECS task definition (or App Runner/Lambda config) so the container sees them as ordinary environment variables — `process.env.DATABASE_URL` works exactly as before and your code does not change. The migration step is a one-time export, a sort into secret-vs-config, and a scripted load so nothing leaks into an image layer or a committed `.env`.

CI/CD is the third piece. The simplest robust pipeline keeps your existing GitHub Actions and adds three steps: build + test, build and push the image to ECR, then update the ECS service (or trigger an App Runner deploy) for a health-checked, zero-downtime rollout. Teams wanting everything in-AWS use CodePipeline + CodeBuild + CodeDeploy (which adds blue/green deploys for ECS out of the box); a Lambda app uses AWS SAM, the Serverless Framework, or AWS CDK. The principle is identical: build an immutable artifact, push it, roll it out with health checks and automatic rollback.

the risky 20 minutes

VIDatabase migration and the cutover: DMS, DNS, and a short downtime window

Everything above can be built and tested without touching production. The database cutover is the one moment that touches live data — where a plan earns its keep. Done right, user-visible downtime is zero to fifteen minutes.

The core technique is to keep the new RDS/Aurora database continuously in sync with your current one while you test, so that at cutover you switch to an already-current copy rather than copying a cold database under time pressure. AWS Database Migration Service (DMS) does exactly this: a full load into RDS/Aurora, then a switch into change data capture (CDC) mode that streams every subsequent change in near-real time. When source and target are the same engine — PostgreSQL → RDS for PostgreSQL, or MySQL → Aurora MySQL — this is a homogeneous migration: no Schema Conversion Tool needed, schema moves as-is. If you are also changing engines, the AWS Schema Conversion Tool (SCT) translates the schema/stored-logic first, and that heterogeneous case is where the real effort lives.

With CDC running and the new Node stack smoke-tested against the synced database, the cutover is short and scripted: enter maintenance (or read-only) mode → let DMS drain the last few seconds → flip `DATABASE_URL` to the RDS/Aurora endpoint and switch traffic to the new Fargate service (or the new API Gateway stage) → update the Route 53 record (TTL lowered ahead of time) → verify writes land in RDS → exit maintenance. Because the data was already synced, the window is dominated by DNS propagation and verification, not data transfer — commonly 0–15 minutes during a low-traffic period, regardless of database size.

The rollback plan is non-negotiable and simple: keep the old environment running until you are confident, and if something is wrong, switch `DATABASE_URL` and DNS back. The clean window is before RDS has accepted production writes the old database has not seen — once committed, rollback means reconciling those writes — so most teams keep the window short (minutes to hours) and watch closely. Lowering DNS TTL to ~60 seconds a day ahead makes both the switch and the rollback fast. For a DynamoDB target the mechanics differ (export/import plus dual-writes, or a Glue/custom backfill) — one more reason a relational-to-DynamoDB move is a refactor, not a cutover.

why DMS over a cold dump

A cold pg_dump | pg_restore (or mysqldump) makes your downtime window = export + transfer + import of the whole database: 20–60 minutes for 5GB, unacceptable at 50GB+. DMS with CDC moves the bulk load before the window and only drains the last few seconds during it — so downtime stays in single-digit minutes regardless of database size. Tiny databases can still skip DMS for a single dump/restore inside the window.

the runbook

VIIThe step-by-step migration (assess → land → cut over → optimize)

Here is the end-to-end sequence a partner runs, mapped to the AWS MAP phases (Assess → Mobilize → Migrate). For a typical Node.js app this is a 2–6 week project — most of it parallelizable, none of it touching production until the final cutover.

  • 1 — Assess (days 1–4) — Inventory every service, the database engine + size + connection pattern, Redis usage (cache vs queues vs pub/sub), WebSockets, scheduled jobs, env vars, and third-party dependencies. Decide the compute split (App Runner / Fargate / Lambda) and RDS vs Aurora vs DynamoDB, and produce the target architecture and a cost before/after model. Under MAP this Assess/TCO phase is typically free.
  • 2 — Build the landing zone — Stand up the AWS account structure, VPC, subnets, security groups, IAM roles, ECR, and Secrets Manager / Parameter Store. (Reuses the AWS Landing Zone and AWS DevOps foundations in the DevOps cluster.)
  • 3 — Containerize (or package functions) — Write the multi-stage Dockerfile(s), split API and worker into separate ECS services, and confirm the image builds, boots, and passes health checks locally and in CI. Package any Lambda edges via SAM/Serverless/CDK.
  • 4 — Provision data + cache — Create the RDS/Aurora instance (and RDS Proxy), the ElastiCache cluster, and any DynamoDB tables in the VPC. Restore the schema, wire EventBridge Scheduler for cron, and add the Socket.IO Redis adapter if you run WebSockets.
  • 5 — Start replication — Run a DMS full load from the current database into RDS/Aurora, then switch to CDC so the new database tracks production while you test. Validate row counts and known records. (Run SCT first if the engine changes.)
  • 6 — Deploy + smoke-test — Deploy the App Runner/Fargate service (and any Lambdas) against the synced database with no public traffic, then run the full test suite + manual smoke tests — including jobs, scheduled tasks, and WebSocket connect/message/disconnect.
  • 7 — Cut over — Run the scripted cutover from the section above (drain CDC → flip DATABASE_URL → switch traffic → update Route 53 → verify → exit) during a low-traffic window. Target: 0–15 minutes.
  • 8 — Optimize + decommission — Apply a Compute Savings Plan, right-size Fargate and RDS, set autoscaling (and Lambda provisioned concurrency if needed), confirm CloudWatch alarms and backups. Keep the old environment as rollback briefly, then decommission it — that is when the savings become real.
what bites Node teams

VIIIGotchas: cold starts, connection pooling, and the Node-specific traps

Most Node.js→AWS migrations that go sideways do so for a small, repeatable set of reasons — and several are specific to how Node and AWS interact. Naming them up front is the cheapest insurance there is.

  • Lambda cold starts on synchronous APIs — A cold Lambda pays for container init plus loading your dependency tree before the first request runs — 200ms–2s+ for a heavy Node bundle, historically worse inside a VPC. Fine for async/bursty work; painful for a latency-sensitive synchronous API. Fixes: provisioned concurrency (removes cold starts but also scale-to-zero savings), aggressive esbuild bundling to shrink init, or — most often — run that API long-running on Fargate instead.
  • Database connection exhaustion — The classic Node-on-AWS outage. A connection-per-request app on autoscaling Fargate, or thousands of concurrent Lambdas each opening a Postgres/MySQL connection, blows past the connection limit. The fix is Amazon RDS Proxy — it pools and multiplexes so a fleet of tasks/functions shares a small set of real connections. Design pooling in; do not discover it during the first traffic spike.
  • Local disk and in-memory state break across instances — Code that writes uploads to local disk, caches in a module-level variable, or stores sessions in process memory works on one server by luck and breaks the instant you run multiple Fargate tasks or Lambda invocations. Move files to S3, shared cache/sessions to ElastiCache, and treat every instance as disposable.
  • WebSockets need a shared adapter once you scale — A single-instance Socket.IO/`ws` server is fine; multiple Fargate tasks are not, unless you add the Socket.IO Redis adapter (ElastiCache) so messages reach clients on other tasks. Otherwise real-time silently works for some users and not others after autoscaling.
  • Cron and scheduled jobs must be recreated explicitly — Whatever ran your scheduled work (in-process node-cron, a crontab) becomes Amazon EventBridge Scheduler invoking an ECS task or Lambda — every job recreated by hand. In-process `node-cron` is especially dangerous: on multiple tasks it fires N times. Move scheduled work to EventBridge so it fires once.
  • Native modules and Lambda's 15-min / payload ceilings — Native add-ons (bcrypt, sharp, node-gyp builds) must be compiled for the runtime architecture (watch arm64 vs x86_64) — build in a matching container, not on a Mac. And mind Lambda's hard limits: 15-minute max execution and ~6MB synchronous payload — long jobs and large responses belong on Fargate or behind S3, not crammed into a function.
compute target, side by side

App Runner vs ECS/Fargate vs Lambda for Node.js

The definitive three-way comparison for running a Node.js app on AWS. Pick the column that matches your workload shape — most production APIs land on Fargate, the simplest web services on App Runner, and spiky/event-driven workloads on Lambda. (Numbers are representative 2026 ranges.)

DimensionApp RunnerECS / FargateLambda + API Gateway
Best forStateless web services, simplest opsProduction APIs, workers, WebSocketsSpiky, event-driven, async edges
Process modelLong-running container (warm)Long-running container (warm)Ephemeral per-invocation function
Cold startsNone (scales from low, not zero by default)NoneYes — 200ms–2s+ unless provisioned concurrency
Scale to zero / idle costScales low; minimal idle costPay per running task (no scale-to-zero)True scale-to-zero, $0 idle
Connection pooling to RDSReused per instanceReused per task (best for pools)Needs RDS Proxy (per-invocation conns)
WebSocketsLimitedYes (ALB + Redis adapter)Via API Gateway WebSocket (different model)
Max execution / requestNo hard limitNo hard limit15 min; ~6MB sync payload
Networking controlConstrained (VPC egress configurable)Full (VPC, SG, ALB, private subnets)Full (VPC-attachable)
Ops surfaceLowest (Heroku-like)Medium (tasks, ALB, autoscaling)Low (no servers; more moving parts)
Cost at steady high trafficLow–moderateLowest for steady load (+ Savings Plan)Can be highest at sustained volume
Cost at spiky / low trafficLowPay for idle tasksLowest (pay per ms)
Migration effort from existing appLowMedium (Dockerfile + task defs)Medium–High (handler/adapter rework)
Rule of thumb: steady, latency-sensitive, connection-pooling synchronous API → Fargate. A few simple stateless web services with minimal ops appetite → App Runner. Bursty, idle-heavy, or event-driven (webhooks, S3 triggers, cron, fan-out) → Lambda. Hybrid (Fargate core + Lambda edges) is common and correct — exactly the allocation a MAP-funded partner produces in the Assess phase.
want this run for you — and funded?
Get matched with an AWS partner who migrates your Node.js app (often MAP-funded)
Start in 3 minutes →
a recent match

A Node.js→AWS migration CloudRoute routed — anonymized

inquiry · seed b2b saas (nestjs api + socket.io), Lisbon
Seed-stage B2B SaaS on a NestJS (TypeScript) REST API plus a Socket.IO real-time layer, PostgreSQL via Prisma, Redis (BullMQ jobs + sessions), running on a single large VM at a generic VPS host. ~$650/month and fragile: one box, manual deploys over SSH, no autoscaling, and real-time that broke whenever they tried to add a second instance.

Situation: They had outgrown the single VM but had no AWS or container experience, and a previous "let's just put it on Lambda" spike had stalled on cold starts and Prisma connection-pool errors against Postgres. They needed a resilient, autoscaling setup without a multi-month DIY effort or a risky big-bang database move — and they wanted real-time to survive horizontal scaling.

What CloudRoute did: Routed within 24 hours to an AWS Advanced-tier partner with a Node/TypeScript track record, who ran the MAP Assess phase (free) and filed the work as a MAP engagement. Target: the NestJS API and the Socket.IO server as two ECS/Fargate services behind an ALB (long-running, warm — not Lambda), Aurora PostgreSQL with RDS Proxy for Prisma pooling, ElastiCache for Redis (BullMQ + sessions + the Socket.IO Redis adapter for cross-task messaging), env vars split into Secrets Manager + Parameter Store, multi-stage Dockerfiles, and GitHub Actions → ECR → ECS rolling deploys. Two spiky edges — image thumbnailing on S3 upload and a nightly export — moved to Lambda. Postgres migrated via AWS DMS (full load + CDC).

Outcome: Cutover ran in a Saturday-night window with ~9 minutes of write-downtime — DMS had Aurora fully in sync, so the switch was DNS + verification, not data transfer. Real-time survived autoscaling (the Redis adapter fixed the multi-instance problem), the Prisma connection errors disappeared behind RDS Proxy, and steady-state AWS spend landed at ~$780/month with autoscaling headroom the single VM never had. Project ran ~4 weeks. Because the workload qualified for MAP, AWS funded the assessment and credited the migration cost — out-of-pocket cost was effectively $0, and CloudRoute's commission was paid by the partner from MAP funding.

project length: ~4 weeks · cutover downtime: ~9 min · architecture: single VM → Fargate (API + Socket.IO) + Aurora + RDS Proxy + ElastiCache + 2 Lambdas · migration cost to customer: ~$0 (MAP-funded)

faq

Common questions

What is the best way to deploy a Node.js app on AWS?
It depends on the workload shape, not on a default. A steady, long-running HTTP API (Express, Fastify, NestJS) that holds a connection pool belongs on Amazon ECS on Fargate (full control) or AWS App Runner (simplest, Heroku-like). Spiky, idle-heavy, or event-driven work — webhooks, S3-triggered processing, scheduled jobs, fan-out — fits AWS Lambda behind API Gateway, which scales to zero and bills per millisecond. Mature systems are often hybrid: a warm Fargate core plus Lambda for the async edges. The common mistake is putting a steady, latency-sensitive synchronous API on Lambda, which surfaces cold-start latency and connection-pool problems.
Should I use Lambda or Fargate (or App Runner) for my Node.js API?
Use Fargate or App Runner when the request benefits from a warm process with an open connection pool — a typical REST/GraphQL API, a WebSocket server, or a worker; no cold-start tax and pooled connections are reused. Use Lambda for bursty, stateless, idle-heavy, or cron-like work, where scale-to-zero and per-ms billing win. App Runner is the lowest-ops choice for a few stateless web services; Fargate is the default for a production app with a real database, workers, or WebSockets.
RDS/Aurora or DynamoDB for a Node.js app?
If your app talks SQL through Prisma, Sequelize, TypeORM, Drizzle, or Knex, move to Amazon RDS or Aurora (PostgreSQL/MySQL-compatible) — schema, queries, and ORM are unchanged because the wire protocol is identical, so it is a true migration. Choose DynamoDB when access patterns are key-value / single-table and you need millisecond latency at very high scale; it pairs especially well with Lambda. The caveat: moving a relational, join-heavy app to DynamoDB is a refactor of your data model, not a migration — so most teams keep relational data on RDS/Aurora and use DynamoDB only for specific high-scale tables.
How do I handle environment variables and secrets on AWS?
Split your flat .env / process.env bag by sensitivity. Real secrets — database credentials, JWT signing keys, API keys — go into AWS Secrets Manager (which can also auto-rotate RDS credentials). Non-sensitive config — log level, public URLs, feature flags — goes into AWS SSM Parameter Store. Both inject into the ECS task definition (or App Runner / Lambda config) so your code still reads them as ordinary environment variables — process.env.DATABASE_URL works exactly as before. Nothing changes in your application; you just stop shipping a committed .env or baking secrets into the image.
How do I deal with Lambda cold starts in Node.js?
Cold starts come from container init plus loading your dependency tree before the first request runs — typically 200ms–2s+ for a heavy Node bundle. For asynchronous or bursty work they usually do not matter. For a latency-sensitive synchronous API you have three levers: provisioned concurrency (keeps functions warm, but removes the scale-to-zero savings), aggressive bundling with esbuild to cut init time, or — most often the right answer — run that API as a long-running container on Fargate or App Runner, where cold starts do not exist.
How do I handle database connection pooling on AWS (Node + RDS)?
Connection exhaustion is the classic Node-on-AWS outage: a connection-per-request app on autoscaling Fargate, or thousands of concurrent Lambdas each opening a database connection, exceeds the database's connection limit. The fix is Amazon RDS Proxy, which pools and multiplexes connections so a large fleet of tasks or functions shares a small set of real ones — effectively mandatory for Lambda + RDS, and strongly recommended for autoscaling Fargate. In long-running containers you also keep a sensibly-sized pool per task. Plan pooling in; do not discover it during your first traffic spike.
What does AWS MAP funding mean for the cost of a Node.js migration?
The AWS Migration Acceleration Program (MAP) is how the migration itself can cost little to nothing. For qualifying migrations — generally those with a meaningful projected AWS spend afterward — AWS funds the Assess phase (typically free) and credits a large share of the migration cost, paying the partner through MAP rather than you. When a workload does not qualify, CloudRoute still routes you to a vetted partner who runs the cutover at a fixed scope. Either way a senior team runs the riskiest parts — the compute-target decision, the DMS cutover, and the connection-pool and real-time design.

Migrate your Node.js app to AWS — run by a partner, often funded by AWS.

CloudRoute routes you to a vetted AWS partner who chooses the right compute target (App Runner / Fargate / Lambda), stands up RDS/Aurora or DynamoDB + ElastiCache, Dockerizes, wires CI/CD, and runs the DMS cutover with RDS Proxy and the WebSocket fixes baked in. Qualifying migrations are MAP-funded, so you get a resilient, autoscaling Node deployment without paying the usual migration bill.

matched within< 24h
project length2–6 weeks
migration cost (MAP)$0–low
Migrate a Node.js App to AWS — Compute, Data, Cutover · CloudRoute