Skip to content

Fix: Heroku H10 App Crashed Error

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

Fix the Heroku H10 App Crashed error by fixing your Procfile, PORT binding, missing dependencies, and build scripts with step-by-step solutions.

H10 App Crashed

I used to treat H10 as the error, and that is exactly why it took me so long to fix the first few. H10 is not the bug; it is the router shrugging because your process already died. The real error, the missing module, the hardcoded port, the unset config var, was printed to the log a line or two above the H10, and the moment I started reading that line instead of the H10 itself, these went from baffling to routine. So everything below is really about finding that line and the handful of causes behind it.

You deploy to Heroku and your app shows:

Application error
An error occurred in the application and your page could not be served.

In the logs (heroku logs --tail), you see:

heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" status=503
heroku[web.1]: State changed from starting to crashed
heroku[web.1]: Process exited with status 1

The H10 error code means your application process crashed immediately after starting.

What H10 Is Really Telling You

Heroku starts your app by running the command in your Procfile. If the process exits for any reason, missing dependencies, incorrect start command, uncaught exceptions, or failure to bind to the assigned port, Heroku marks it as crashed and returns H10 to all incoming requests.

Unlike local development, Heroku assigns a dynamic port via the PORT environment variable. Hardcoding a port like 3000 causes the app to start but become unreachable, which Heroku interprets as a crash. The router watches your dyno’s stdout for a “binding to port” signal within a 60-second boot window. If your code listens on 127.0.0.1 instead of 0.0.0.0, the port appears open inside the dyno but not from the router’s perspective, so the dyno is killed and replayed as H10 on the next request.

H10 is a tail-end symptom. The actual failure is almost always one of five root causes, and Heroku’s log line for H10 is the same regardless of which one fired: missing or wrong Procfile, wrong PORT binding, missing runtime dependency, oversized slug exceeding the 500MB compile limit, or memory exhaustion (R14/R15) that crashed the process and surfaced as H10 on the very next request. Reading only the H10 line and restarting the dyno is the most common dead end. The actionable information is the traceback printed just above the crash line, plus the heroku ps and heroku releases output that tells you which slug actually booted.

Diagnostic Timeline

When the H10 alert lands in Slack at 2 a.m., follow this sequence instead of restarting blindly.

Minute 0: Stream the logs, do not just look at the dashboard. Run heroku logs --tail --app your-app. The web UI truncates the stack trace. The tail captures stderr from the process up to the moment it died, and that traceback is your real error.

Minute 1: Read the line directly above State changed from starting to crashed. That line is the actual crash. H10 is the router’s reaction to your process exiting. Cannot find module 'pg' and ImportError: No module named X are the most common offenders.

Minute 3: Confirm the slug actually changed. Run heroku releases and verify the latest v<N> matches the commit you just pushed. If you pushed to GitHub but did not push to Heroku, you are debugging an old slug. Check git remote -v and git push heroku main.

Minute 5: Reproduce the boot locally with the exact Procfile. Install the Heroku CLI and run heroku local web. This runs your Procfile under the same environment shape Heroku uses. If it fails locally, you skip 95% of the remote debugging round-trip.

Minute 8: Validate the PORT binding. Search your start command for any hardcoded port. The line must read from process.env.PORT (Node), os.environ['PORT'] (Python), or $PORT (Procfile). Listening on localhost or 127.0.0.1 counts as broken, bind 0.0.0.0.

Minute 12: Check for the silent killers: slug size and memory. Run heroku ps. If you see R14 or R15 above the H10, you are out of memory and the OOM kill is restarting your dyno into a crash loop. Run heroku apps:info and check the slug size, anything over 500MB will fail to deploy.

Minute 18: Open heroku run bash and run your start command by hand. This bypasses the Procfile and runs in the same one-off dyno environment as your web dyno. If the command fails here, you have the smallest possible reproducer and the full error in front of you.

Fix 1: Create or Fix Your Procfile

The Procfile tells Heroku how to start your app. It must be in the root directory with exact capitalization (Procfile, not procfile or PROCFILE):

web: node server.js

For common frameworks:

# Node.js / Express
web: node server.js

# Node.js with npm script
web: npm start

# Python / Django
web: gunicorn myproject.wsgi

# Python / Flask
web: gunicorn app:app

# Ruby / Rails
web: bundle exec rails server -p $PORT

# Java / Spring Boot
web: java -jar target/myapp.jar --server.port=$PORT

If you don’t have a Procfile, Heroku tries to detect your framework automatically. This often fails or runs the wrong command. Always create an explicit Procfile.

Verify the Procfile is tracked by Git:

git ls-files Procfile
# Should output: Procfile

One thing that has tripped me up more than I would like to admit: Heroku builds only what Git tracks. If your Procfile is untracked, gitignored, or you simply forgot to commit it, Heroku never sees it and falls back to guessing your start command, which is a frequent road to H10. A quick git ls-files Procfile tells you whether Heroku will actually find it.

Fix 2: Bind to the Correct PORT

Heroku assigns a dynamic port through the PORT environment variable. Your app must listen on this port:

// Node.js / Express
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
# Python / Flask
if __name__ == '__main__':
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port)
# Python / Django (gunicorn handles this automatically)
# In Procfile:
web: gunicorn myproject.wsgi --bind 0.0.0.0:$PORT

Common mistakes:

// Wrong - hardcoded port
app.listen(3000);

// Wrong - listening on localhost only
app.listen(PORT, '127.0.0.1');

// Correct - listen on all interfaces
app.listen(PORT, '0.0.0.0');

Heroku expects your app to bind to $PORT within 60 seconds of starting. If it takes longer, the dyno is killed with an R10 (Boot Timeout) error, a distinct code from H10 that specifically means “the web dyno never bound to its port in time.”

Fix 3: Fix Missing Dependencies

Dependencies listed in devDependencies are not installed by default on Heroku. If your app needs them at runtime, move them to dependencies:

{
  "dependencies": {
    "express": "^4.18.0",
    "dotenv": "^16.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

For a build step like TypeScript compilation, you usually need no special config at all. The Heroku Node.js buildpack installs devDependencies during the build, runs your build/heroku-postbuild script, and only then prunes devDependencies before the dyno boots. So tsc in a build script works out of the box.

What actually matters is putting each package in the right bucket:

  • Needed only to build (typescript, webpack, bundlers): leave it in devDependencies. It is present during the build and correctly removed from the running dyno.
  • Needed at runtime: it must be in dependencies.

Do not reach for NODE_ENV=development to force devDependencies into production. That also flips your app into development mode at runtime, disabling production optimizations and leaking dev-only behavior, and it keeps every devDependency in the slug. If a package is genuinely needed at runtime, promote that one package to dependencies instead.

For Python, ensure your requirements.txt is complete:

pip freeze > requirements.txt

Check for native dependencies that need buildpacks:

# Add buildpack for apps needing specific system libraries
heroku buildpacks:add --index 1 heroku-community/apt

Fix 4: Fix Build Scripts

If your app requires a build step (TypeScript compilation, frontend bundling), configure it properly:

{
  "scripts": {
    "build": "tsc && webpack --mode production",
    "start": "node dist/server.js"
  }
}

Heroku runs your scripts in this order:

  1. heroku-prebuild (if defined)
  2. dependency install (npm install / yarn / pnpm)
  3. the build step
  4. heroku-postbuild (if defined)
  5. prune devDependencies
  6. boot the Procfile command

Two things people get wrong about the build step. First, Heroku has run a build script automatically since 2020, not a recent change, so you rarely need heroku-postbuild at all, a plain build script is enough. Second, the two scripts behave differently across buildpacks. On the Classic Node.js buildpack, heroku-postbuild runs instead of build (define both and your build is silently ignored). On the newer Cloud Native Buildpack, build and heroku-postbuild run in sequence, so defining both where postbuild just calls npm run build compiles your app twice. The safe default is to define only build and skip heroku-postbuild unless you genuinely need a Heroku-only build customization.

If your build creates output in dist/ but your Procfile points to src/server.js, the app crashes because the compiled files don’t exist:

# Wrong - source file, not compiled
web: node src/server.js

# Correct - compiled output
web: node dist/server.js

The build-step mistake I see most is expecting Heroku to compile TypeScript on its own. It will not: there is no global tsc on a dyno. TypeScript has to be a dependency in package.json and it has to be invoked by a build (or heroku-postbuild) script. If your Procfile points at dist/server.js but nothing ran tsc, that file never exists and you get H10 on boot.

Fix 5: Fix Dyno Startup Issues

Check the actual crash reason in the logs:

heroku logs --tail --app your-app-name

Look for the error message before the crash line:

Error: Cannot find module './config/database'
heroku[web.1]: Process exited with status 1
heroku[web.1]: State changed from starting to crashed

Common startup errors:

# Missing module
Error: Cannot find module 'express'
# Fix: npm install express --save

# Syntax error
SyntaxError: Unexpected token 'export'
# Fix: Use CommonJS or configure ESM properly

# Missing environment variable
TypeError: Cannot read properties of undefined (reading 'split')
# Fix: Set config vars on Heroku

Restart the dyno after making changes:

heroku restart --app your-app-name

Fix 6: Handle Memory Limits

Heroku’s entry-level dynos, Eco and Basic, have 512 MB of RAM, and so does Standard-1X; Standard-2X gives you 1 GB. (Heroku removed its free dynos on November 28, 2022 and renamed the old Hobby plan to Basic, so any guide that still says “free dyno” predates that change.) If your app exceeds its dyno’s memory, it is killed with an R14 (memory quota exceeded) or R15, which can surface as H10 on the next restart:

# Check memory usage
heroku logs --tail | grep "memory"

Reduce memory usage:

// Node.js - limit heap size
// In Procfile:
web: node --max-old-space-size=460 server.js
# Python - use fewer Gunicorn workers
# In Procfile:
web: gunicorn myapp:app --workers 2 --threads 2

For Node.js memory issues, common culprits are:

  • Loading large datasets into memory
  • Not closing database connections
  • Memory leaks from event listeners
  • Large npm packages

Fix 7: Configure Environment Variables (Config Vars)

Missing config vars are a silent killer. Your app starts, can’t find required configuration, and crashes:

# Set config vars
heroku config:set DATABASE_URL=postgres://...
heroku config:set SECRET_KEY=your-secret-key
heroku config:set NODE_ENV=production

# View current config
heroku config

# Remove a config var
heroku config:unset DEBUG

Don’t commit .env files. Use Heroku config vars instead:

# Copy from .env to Heroku
while IFS='=' read -r key value; do
  heroku config:set "$key=$value"
done < .env

Verify your app handles missing variables gracefully:

const requiredVars = ['DATABASE_URL', 'SECRET_KEY', 'API_KEY'];
for (const varName of requiredVars) {
  if (!process.env[varName]) {
    console.error(`Missing required environment variable: ${varName}`);
    process.exit(1);
  }
}

Fix 8: Fix Buildpack Issues

Wrong or missing buildpacks cause the app to build incorrectly:

# Check current buildpacks
heroku buildpacks

# Set the correct buildpack
heroku buildpacks:set heroku/nodejs
heroku buildpacks:set heroku/python

# Multiple buildpacks (order matters)
heroku buildpacks:add --index 1 heroku/nodejs
heroku buildpacks:add --index 2 heroku/python

If Heroku auto-detects the wrong language (e.g., it sees both package.json and requirements.txt), set the buildpack explicitly.

For monorepos where the app isn’t in the repo root, Heroku has no built-in subdirectory setting. You add a monorepo buildpack, and the config-var name depends on which one you pick, common choices are APP_BASE (lstoll), BUILD_SUBDIR (croaky), and APP_PATH (tobius):

heroku buildpacks:add --index 1 https://github.com/lstoll/heroku-buildpack-monorepo
heroku config:set APP_BASE=packages/api

There is no Heroku-native PROJECT_PATH variable, so always check your chosen buildpack’s README for the exact name it reads.

Clear the build cache if you changed buildpacks:

heroku plugins:install heroku-repo
heroku repo:purge_cache --app your-app-name
git push heroku main

Crash Causes That Hide Below the H10 Line

  • Run the app locally with production settings. Use NODE_ENV=production npm start or heroku local with your Procfile to reproduce the issue.

  • Check for Heroku-specific limitations. Heroku has an ephemeral filesystem, files written to disk are lost on dyno restart. Use S3 or similar for file storage.

  • Try scaling down and up. heroku ps:scale web=0 then heroku ps:scale web=1 forces a fresh start.

  • Check your Git remote. Ensure you’re pushing to the correct Heroku app: git remote -v.

  • Look for port binding race conditions. If your app performs async initialization before calling listen(), it might time out. Bind the port first, then initialize.

  • Review the release phase. If you have a release command in your Procfile that fails, it prevents the new release from deploying: release: python manage.py migrate.

  • Check the slug size. Run heroku apps:info and look at “Slug size.” Anything approaching 500MB is dangerous. Move large model files, fonts, or media to S3 and download at boot. A node_modules directory full of transitive ML libraries is a common culprit and will fail with H10 long before R14 fires.

  • Audit your .profile.d scripts. Files in .profile.d/*.sh run before your Procfile and can crash the dyno before your app starts. A buggy export in .profile.d/setup.sh produces an H10 with no error visible in your application logs. Comment out the script and redeploy to isolate.

  • Look for nondeterministic startup races. If you start the HTTP server inside an async unhandled-rejection handler, the listener may register after Heroku’s 60-second window. Call app.listen(PORT) synchronously at the top of your bootstrap and run async warmup afterwards.

  • Rule out conflicting port environment variables set by buildpacks. Some community buildpacks set conflicting PORT defaults. Run heroku run env | grep PORT and verify only one PORT is set, matching what your start command reads.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles