Heroku Review Apps: Practical Trade-offs for Ephemeral Environments
Heroku Review Apps give every pull request a full, isolated environment—but they bring cost, secrets, and configuration trade-offs. A practical guide to app.json, CI integration, and the limits you'll hit at scale.
08 Oct 2025, 11:57 UTC

The Problem: Staging Bottlenecks and Shared Environments
Teams adopting continuous delivery often hit a wall with staging: a single shared environment becomes a contention hotspot. Developers wait for deployments, tests collide, and data pollution makes debugging unreliable. Heroku Review Apps promise a fix—spin up a full, isolated environment for every pull request automatically. But the promise comes with real trade-offs: cost, configuration complexity, and operational surprises that only surface at scale.
How Review Apps Actually Work
When you connect a Heroku Pipeline to a GitHub repository and enable Review Apps, Heroku creates a new Heroku application for each open pull request. This isn't a lightweight preview—it's a full app with its own dynos, add-ons, and config vars, built from the same codebase and buildpack as your production app. The Review App inherits the parent app's configuration unless you override it via app.json.
The lifecycle is tied to the PR: open a PR, Heroku provisions the app; merge or close the PR, Heroku tears it down. By default, inactive Review Apps are destroyed after 5 days. You can adjust this TTL in Pipeline settings or via CLI.
Configuring a Realistic Stack with app.json
The app.json manifest in your repository root controls what each Review App looks like. Here's a practical example for a typical Django/PostgreSQL application:
{
"name": "MyApp Review",
"scripts": {
"postdeploy": "python manage.py migrate --noinput && python manage.py seed_review_data"
},
"addons": [
"heroku-postgresql:essential-0",
"heroku-redis:mini"
],
"formation": {
"web": {
"quantity": 1,
"size": "eco"
},
"worker": {
"quantity": 0
}
},
"env": {
"DJANGO_SETTINGS_MODULE": {
"value": "config.settings.review"
},
"SECRET_KEY": {
"required": true
},
"DATABASE_URL": {
"required": true
}
}
}Key decisions in this manifest:
- Add-ons:
essential-0PostgreSQL andminiRedis are the lowest-cost plans that support ephemeral provisioning. Some enterprise add-on plans block Review App creation entirely—check the add-on documentation before committing. - Formation: One
ecodyno for web, zero workers. This keeps costs predictable (~$5/month per Review App if running 24/7). The free/hobby tiers are deprecated;ecois the new baseline. - Environment variables:
SECRET_KEYandDATABASE_URLare markedrequired: truewith no default. This forces your CI pipeline to inject them as secrets, preventing accidental leakage of production values. The Review App gets its own database URL from the provisioned add-on. - Postdeploy script: Runs migrations and a custom seed command. Keep migrations idempotent and fast—this runs on every Review App creation. Heavy seeding slows PR turnaround; consider a separate lightweight seed for review environments.
Integrating with CI for End-to-End Tests
The real value emerges when your CI pipeline deploys to the Review App and runs tests against it. A typical GitHub Actions workflow:
jobs:
review-app-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Heroku Review App
uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
heroku_app_name: ${{ github.event.pull_request.head.repo.name }}-pr-${{ github.event.number }}
heroku_email: ${{ secrets.HEROKU_EMAIL }}
- name: Run Cypress E2E tests
run: npm run cypress:run -- --config baseUrl=https://${{ github.event.pull_request.head.repo.name }}-pr-${{ github.event.number }}.herokuapp.com
env:
CYPRESS_BASE_URL: https://${{ github.event.pull_request.head.repo.name }}-pr-${{ github.event.number }}.herokuapp.comThe Review App URL follows a predictable pattern: <parent-app-name>-pr-<pr-number>.herokuapp.com. Your CI can construct it from the PR event payload. Heroku also exposes HEROKU_APP_NAME and HEROKU_APP_URL as environment variables during the build.
Trade-offs and Limitations
Cost Accumulation
Each Review App consumes dyno hours and add-on resources. Ten concurrent PRs with eco dynos and Essential PostgreSQL run ~$50/month if active continuously. Teams often underestimate this because the 5-day TTL feels generous—until a sprint generates 30 long-lived PRs. Mitigations:
- Set aggressive TTL (1-2 days) in Pipeline settings.
- Use
formationto disable workers and non-critical processes. - Monitor via Heroku's team billing dashboard; set up alerts for dyno-hour thresholds.
Secrets Management
By default, Review Apps copy all parent app config vars. If production STRIPE_SECRET_KEY is set on the parent, every Review App gets it. The app.json env section with required: true and no default is your primary defense—it forces explicit injection via CI secrets. But you must audit the parent app's config vars and ensure none contain sensitive values that shouldn't be replicated.
Private Spaces and Compliance
If your production app runs in a Private Space (for VPC peering, compliance, or data residency), Review Apps can inherit that isolation—but only if the Pipeline itself is created inside the Private Space. This adds 3-5 minutes of provisioning latency per Review App (vs. ~30 seconds in Common Runtime) and requires Enterprise-tier Private Space access. Public GitHub repos cannot trigger Private Space Review Apps; the repository must be accessible via your Heroku GitHub App installation with appropriate permissions.
Monorepo Complications
Review Apps build from the repository root using a single buildpack. In a monorepo with multiple services (e.g., apps/api, apps/web, apps/worker), a PR touching only the web frontend still triggers a full build of all services unless you configure separate Pipelines per service or use a custom buildpack with path-based logic. This is a common source of wasted build minutes and flaky Review Apps.
Actionable Checklist for Adoption
- Audit parent app config vars: Remove or rotate any secrets that shouldn't appear in Review Apps. Use
app.jsonrequired: truefor all sensitive keys. - Start with a minimal app.json: One web dyno, essential add-ons, fast migrations. Add complexity only when you have a measured need.
- Set TTL to 1 day initially: Observe actual usage patterns before relaxing.
- Add a CI gate: Require successful Review App deployment and test pass before merge. Heroku's "Review App configuration" PR checks integrate with GitHub Actions, CircleCI, and others.
- Test destruction: Open a test PR, verify the Review App appears in
heroku reviewapps:list -p <pipeline>, merge the PR, confirm automatic cleanup and no orphaned add-ons.
Review Apps eliminate the staging bottleneck, but they shift operational burden to configuration discipline and cost awareness. Treat app.json as infrastructure-as-code: version it, review it, and test its behavior in a non-production Pipeline before rolling out to the team.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.