Streamlining Your Migration: How to Efficiently Move from Heroku to AWS with Minimal Downtime
Heroku enables rapid prototyping and quick launches with clean abstractions. But eventually, teams hit scaling limitations, unpredictable billing, and restricted configuration. AWS—though more complex—offers fine-grained control, cost optimization, and architectural flexibility. The trade-off: orchestration overhead and a steeper learning curve.
Faced with needing to run a Rails app at scale—100GB+ Postgres, Redis caching, some nightly Sidekiq batch jobs—I encountered all the usual Heroku sticking points: noisy neighbor resource limits, the inability to tweak instance internals, and a growing monthly bill. Migration to AWS followed, with emphasis on minimal downtime and data integrity. Here’s what actually worked.
1. Audit and Blueprint Existing Heroku Workloads
Start with facts, not guesses.
- Enumerate all dynos, buildpacks, add-ons, and OAuth integrations. Capture versions and billing plans.
- Pull environment with
heroku config -a your-app > .env.current
- Log Heroku Postgres size and active connections.
heroku pg:info -a your-app
- List all scheduled tasks (review Heroku Scheduler, review any
Procfile
definitions).
Side note: Add-ons like Papertrail or external SMTP services rarely migrate cleanly. Expect to re-architect these on AWS.
2. Map Each Component to AWS Equivalents
Abstraction vanishes. You’re now responsible for networking, IAM, and deployment patterns.
Typical mappings:
Heroku Component | AWS Analog |
---|---|
Web dynos | ECS/Fargate, EKS, or EC2 |
Heroku Postgres | Amazon RDS (Postgres) |
Heroku Redis | ElastiCache Redis |
Scheduler/Workers | ECS Scheduled Task, Lambda, or EC2 Cron |
ENV/config vars | SSM Parameter Store, Secrets Manager |
- Draw a component diagram. If the app is containerized (Dockerized on Heroku CI), ECS is a natural fit. Otherwise, EC2 (with systemd for process management).
- Networking: design a VPC with public/private subnets, at least two AZs. Security Groups ≠ Heroku’s ephemeral routing—plan layer 7/4 firewalling and IAM roles tightly.
- Load balancing: ALB covers HTTP(S); NLB if raw TCP is needed.
Tip: Start IaC from day one using Terraform (>=1.0 recommended) or AWS CloudFormation. Retrofit is painful.
3. Stand Up a Parallel Environment
No big-bang cutovers. Bring up a full AWS stack while Heroku stays live.
- Provision RDS with multi-AZ and backups enabled.
- Build/deploy containers to ECS using ECR as registry (or deploy code to EC2 directly for non-containerized workloads).
- Use
heroku pg:backups:capture
→ fetch to local, thenpg_restore
into RDS:
(Note: indexes and extensions sometimes require manual reconciliation.)heroku pg:backups:download -a your-app pg_restore --no-owner -d postgresql://user:password@aws-rds-endpoint/mydb latest.dump
- Move ENV variables/secrets:
aws ssm put-parameter --name "/myapp/DB_URL" --value "postgres://..." --type "SecureString"
- Install a metrics/monitoring agent early (Datadog, Prometheus sidecar, or CloudWatch agent).
Known issue: Out-of-order migration of environment variables leads to silent auth failures. Triple-check deployments.
4. Data Migration with Ongoing Sync
Databases rarely freeze for hours without consequence. Opt for staged migration.
- Initial bulk data dump brings up main tables.
- Set up logical replication (
pglogical
, AWS DMS) before user traffic resumes. This enables ongoing changes to flow from Heroku Postgres → AWS RDS. - For Redis, use
redis-cli --rdb
to export a snapshot, but expect cache misses post-migrate. - Large tables (>10M rows): Parallelize imports, disable triggers as needed, re-enable constraints after.
Practical error:
ERROR: extension "pg_stat_statements" already exists
Often encountered during restore; safe to DROP EXTENSION ...
before re-playing schema, if extension was preinstalled.
5. Controlled Cutover: Traffic Shifting
- Set DNS TTL to 60 seconds at least 24 hours before migration.
- Smoke-test on AWS with a subset of internal or beta users. Dark launching behind a feature flag is effective.
- Blue/green deployment: keep Heroku as blue, AWS as green. Validate logs and error rates as DNS change propagates.
- Watch for sticky sessions; ALB by default is stateless unless configured otherwise.
- Gotcha: If you use Cloudflare or a CDN, cache may mask real AWS-origin failures for up to an hour post-switch.
Sample Route 53 weighted policy for 10% test traffic:
- Name: api.mydomain.com
Type: A
RoutingPolicy: Weighted
Records:
- Value: 18.223.17.4
Weight: 90 # Heroku
- Value: 3.45.183.76
Weight: 10 # AWS
TTL: 60
6. Finalize, Monitor, Decommission
- Final incremental DB/Redis sync
- Toggle production DNS
- Monitor in real time. Expect up to 5% of users to be briefly impacted, especially during DNS propagation or if clients ignore TTL
- Keep Heroku add-ons running (with billing minimized) for an agreed fallback window (typically 72h)
- Migrate/no-op any remaining ephemeral storage or attached services (e.g., S3 assets)
After one week of stable AWS operations, terminate Heroku dynos and add-ons. Update runbooks and incident response docs.
Additional Lived Experience
- IAM role misconfigurations caused most initial outages, not application bugs.
- VPC endpoints for RDS and S3 generally outperform legacy NAT approaches.
- Automated smoke tests (e.g., Cypress, or
curl
against key endpoints from multiple regions) catch regressions earlier than manual checks.
Non-obvious: Heroku’s log drain format doesn’t match CloudWatch. Custom parsers may be necessary if you depend on log-based metrics.
Migrating from Heroku to AWS requires careful inventory, meticulous data movement, and staged go-live validation. Avoid “lift and shift” dogma; smooth transitions are built on parallelism and small, testable changes. The result: a predictable, scalable infrastructure—at the cost of greater operational responsibility.
Questions about the process, or experiences with specific tools like DMS or ECS? Happy to share Terraform modules or migration playbooks that have worked in production.