EthioDeploy
DocsPricingDomainsBlogToolsChangelog

Blog/Fix deploy errors

Heroku H10 App crashed: causes and fixes

BS
Brook SolomonPublished November 2, 2026
Share on XShare on LinkedIn
code=H10

THE SHORT ANSWER

Heroku logs H10 App crashed when a request reaches your app and every web dyno has crashed. H10 is the symptom, not the cause. The real error is in the app's own log lines just above the router line, and it is usually one of five things: the app didn't bind to $PORT within 60 seconds, the Procfile is wrong, a package was pruned with devDependencies, a config var or database is missing at boot, or the dyno ran out of memory.

What does H10 App crashed mean?

Heroku's error code reference says: “A crashed web dyno or a boot timeout on the web dyno will present this error.” The router couldn't hand the request to any web dyno, so the visitor sees Heroku's Application Error page and the router logs a line like this:

heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" host=example-app.herokuapp.com fwd=17.17.17.17 dyno= connect= service= status=503 bytes=

The empty dyno= is the giveaway: no dyno was available to take the request. The line tells you nothing about why. The crash itself is logged earlier, by your app and by the dyno manager. In Heroku's own example the cause is a missing gem, printed two seconds before the router line:

heroku[web.1]: State changed from down to starting
app[web.1]: Starting process with command: `bundle exec rails server -p 22020`
app[web.1]: Missing the Rails 2.3.5 gem. Please `gem install -v=2.3.5 rails`
heroku[web.1]: Process exited
heroku[router]: at=error code=H10 desc="App crashed" ... status=503

Only web dynos produce H10. A crashed worker dyno never does, because workers don't receive HTTP requests. The numbers in this post are for Heroku's Cedar generation (the Common Runtime most apps run on); Fir apps differ in a few places, noted below.

Quick checklist

Log line above the H10CauseFix
Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launchThe app listens on a fixed port, or does too much work before it starts listeningRead process.env.PORT, and move build steps out of the start command
Error: Cannot find module '...'The package is in devDependencies, which Heroku prunes after the buildMove it to dependencies
Process exited with status 0 right after startingThe web command runs something that finishes, such as a migration or a buildMake the web: line start a server; put migrations in release:
Python ModuleNotFoundError from gunicornThe module path in the Procfile doesn't match your codeUse gunicorn module:app or gunicorn project.wsgi
A connection error, or a KeyError / undefined for a settingA config var or the database isn't available when the app bootsCheck heroku config and the add-on's status
Error R15 (Memory quota vastly exceeded)The dyno was killed for using too much memoryLower worker count or memory use, or use a bigger dyno

How do I find the real error?

Start with the logs, then look at the dyno state:

# Stream logs, then load the page again to trigger a fresh boot
heroku logs --tail

# Only your app's own output, without router lines
heroku logs --source app --tail

# Dyno state: up, starting, crashed
heroku ps

# Force a restart so the boot error shows up again in the tail
heroku ps:restart

On Cedar, heroku logs shows the last 100 lines by default and up to 1,500 with -n. If the crash scrolled past, restart the dyno with the tail open. Fir apps keep no log history, so heroku logs there is always a live tail.

Two more ways to see the crash directly:

  • heroku run bash opens a one-off dyno with your built app. Run the command from your web: line by hand and the error prints in your terminal. One-off dynos can't receive HTTP traffic, so this is for reading errors, not for testing requests.
  • heroku local web runs the Procfile's web process on your machine, reading a local .env file. It catches Procfile typos before you push.

If the crash started with your last deploy, heroku releases lists recent releases and heroku releases:rollback puts the previous code and config vars back while you fix it. A rollback doesn't touch add-ons or data stored in them, so a database migration stays applied.

Does your app bind to $PORT in time?

This is the most common cause. Heroku assigns each web dyno a port in the PORT environment variable. If the process hasn't bound to it within 60 seconds of launch, Heroku kills it with error R10, and every request after that gets H10. Java apps built with Gradle or Maven get 90 seconds.

// Node / Express
const port = process.env.PORT || 3000
app.listen(port)

# Procfile, Python (gunicorn binds 0.0.0.0:$PORT on its own when PORT is set)
web: gunicorn myproject.wsgi

# Procfile, uvicorn
web: uvicorn main:app --host 0.0.0.0 --port $PORT

Calling app.listen(port) with no host is fine in Node: it listens on all interfaces. Passing "localhost" or "127.0.0.1" is not, because the router connects from outside the process. The full list of framework defaults is in 502 Bad Gateway after deploy.

If the app does bind to PORT and still hits R10, it is too slow to start. Heroku names two reasons: waiting on an external resource such as a database, and doing too much work during startup. A start script that runs webpack, a TypeScript compile or a Rails asset build is the usual example. Move that work into the build (a build or heroku-postbuild script for Node) so the start command only starts the server.

Is the Procfile right?

The web process type is the only one that receives HTTP traffic, so the web: line has to start a server that keeps running. Heroku's rules for the file:

  • It is named Procfile, with a capital P and no extension. Procfile.txt and procfile don't count.
  • It sits in the root of the repo, not in a subfolder.
  • Each line is <process type>: <command>.
web: gunicorn myproject.wsgi
release: python manage.py migrate

Heroku counts a process that exits with status 0 as a crash too. A web: line like python manage.py migrate && gunicorn myproject.wsgi works, but web: python manage.py migrate alone runs, exits and crashes the dyno. Migrations belong in the release: line: if the release command fails, Heroku doesn't deploy that release and the current one keeps running.

Without a Procfile, a Node app runs its start script from package.json. Check that npm start runs the production server and not a dev server or a build.

Was a package pruned with devDependencies?

Heroku's Node.js build installs dependencies and devDependencies, runs your build script, then removes everything in devDependencies before the app starts. A package your server imports at runtime that is listed as a dev dependency works during the build and then crashes the dyno with Cannot find module.

npm install --save some-package   # moves it to dependencies

Heroku also documents a switch to skip pruning, heroku config:set NPM_CONFIG_PRODUCTION=false, but moving the package is the real fix: it keeps the slug smaller and makes the dependency list say what the app needs.

While you're in package.json, check engines.node. Without it Heroku picks its default Node version (24.x today), which may not be the one you develop on.

Is a config var or the database missing at boot?

Config vars set on your laptop in a .env file don't exist on Heroku until you set them. An app that reads a missing variable at startup, or tries to open a database connection with it, exits before binding to PORT.

heroku config                     # what the dyno actually has
heroku config:set SECRET_KEY=...   # restarts the dynos with the new value

Setting or changing a config var restarts the dynos, so you'll see straight away whether it fixed the crash. If the database is the problem, check the add-on is provisioned and that the app reads DATABASE_URL rather than a host and password from your local setup.

Did the dyno run out of memory?

Look for R14 and R15 above the H10. R14 (Memory quota exceeded) means the dyno is swapping and slow but still running. R15 (Memory quota vastly exceeded) means Heroku killed it with SIGKILL, which is a crash. For Python, gunicorn reads WEB_CONCURRENCY, which Heroku sets from the dyno size; lowering it runs fewer workers and uses less memory. For Node, look for a large in-memory cache or a leak, or move to a dyno with more memory.

Why does H10 keep showing after I fixed it?

Heroku restarts a crashed dyno once straight away. If it crashes again it waits: up to 20 minutes before the next attempt, then up to 40, 60, 180 and 320 minutes. So an app can keep returning H10 for a while after the underlying problem (say, a database outage) has cleared.

The wait resets when the dyno starts correctly, when you push a new release, when you restart the app, or when you scale web dynos to 0 and back. After a fix, deploy or run heroku ps:restart instead of waiting. Private Spaces restart continuously with no cool-off, and Fir apps use a different backoff.

H10 vs H12, H13, H14 and H20

CodeMeaningWhere to look
H10 App crashedWeb dynos exist but have crashed or failed to bootThe app's log lines before the crash
H12 Request timeoutA request took longer than 30 secondsSlow endpoints; move long work to a worker
H13 Connection closed without responseThe app accepted the connection and closed it without answeringServer timeouts shorter than 30 seconds, killed worker processes
H14 No web dynos runningThere are no web dynos at allheroku ps:scale web=1
H20 App boot timeoutNo web dyno reached the up state within 75 secondsSlow startup; same fixes as R10

What happens on EthioDeploy?

The same mistakes crash an app on any platform, but on EthioDeploy a crash shows up during the deploy instead of in front of your visitors. Every deploy waits for a health check: a GET to / that has to return a 2xx or 3xx status within 30 seconds. If the new version crashes or never answers, the deploy fails and the previous version keeps serving traffic.

The deploy log then says what happened, with the exit code:

The app's process exited (code 1) and was restarted 3 times. Check the container logs above for the error.

EthioDeploy sets PORT and HOSTNAME=0.0.0.0 for you, reads the web line of a Procfile as the start command, and imports Heroku config vars with ethiodeploy env push .env. If you're moving off Heroku, see Heroku alternatives and EthioDeploy vs Heroku.

Questions

H10 means every web dyno has crashed or failed to boot. The common causes are not binding to $PORT within 60 seconds (error R10), a wrong or missing Procfile web line, a package removed with devDependencies, a missing config var or database at startup, and running out of memory (R15). The real error is in the app's log lines just before the H10.

Run heroku logs --tail, then reload the app or run heroku ps:restart so the dyno boots again. Read the lines from app[web.1] and heroku[web.1] just before the router's H10 line, fix that error, and deploy. A new release resets Heroku's restart cool-off, so the fix takes effect straight away.

No. R10 is a boot timeout: the web process didn't bind to $PORT within 60 seconds and Heroku killed it. H10 is what the router logs for requests after that, because no web dyno is running. R10 is one cause of H10.

No. H10 only happens when a web request reaches an app whose web dynos have all crashed. A crashed worker dyno shows up in heroku ps and in the logs, but never as H10.

Usually because something your laptop provides is missing on Heroku: a fixed port instead of $PORT, a package in devDependencies that Heroku prunes, a variable from a local .env file, or a local database. Run heroku local web to test the Procfile on your machine, and heroku config to see what the dyno actually has.

H10 means web dynos exist but have crashed. H14 means there are no web dynos at all, usually because they were scaled to 0. Fix H14 with heroku ps:scale web=1.

Sources

Checked on November 2, 2026:

  • Heroku: Error codes (H10, H12, H13, H14, H20, R10, R14, R15)
  • Heroku: Dyno startup behavior
  • Heroku: Dyno restarts
  • Heroku: Logging
  • Heroku: CLI commands
  • Heroku: One-off dynos
  • Heroku: heroku local
  • Heroku: Releases and rollback
  • Heroku: The Procfile
  • Heroku: Release phase
  • Heroku: Node.js builds (devDependencies pruning)
  • Heroku: Node.js support
  • Heroku: Gunicorn on Heroku
  • Heroku Help: Could H10 errors be produced by non-web dynos?
  • Heroku Help: Why is my Node.js app crashing with an R10 error?
  • Node.js: server.listen
  • Gunicorn: bind setting

Push a repo. Get a URL.

  • Heroku alternatives
  • EthioDeploy vs Heroku
  • Troubleshooting deploys
Start free

Join the EthioDeploy Telegram

Release notes as they ship, plus deploy help from the team and other developers.

Join on Telegram
BS
WRITTEN BYBrook SolomonFounder, EthioDeploy

Brook Solomon founded EthioDeploy and builds its deploy pipeline, build servers and AI coding agent. The posts here come from errors seen in real deploys on the platform.

All posts by Brook →
On this page
  • What does H10 App crashed mean?
  • Quick checklist
  • How do I find the real error?
  • Does your app bind to $PORT in time?
  • Is the Procfile right?
  • Was a package pruned with devDependencies?
  • Is a config var or the database missing at boot?
  • Did the dyno run out of memory?
  • Why does H10 keep showing after I fixed it?
  • H10 vs H12, H13, H14 and H20
  • What happens on EthioDeploy?
  • Questions
  • Sources
0% read

Keep reading

  • /* → index.html

    Fix deploy errorsNov 4, 2026

    404 on page refresh in a React or Vite SPA

    A single-page app returns 404 on refresh because the browser asks the server for a path like /dashboard, and the server has no file at that path. Your router only exists in the JavaScript that index.html loads, so the server has to answer every unknown path with index.html and let the router take over. It works locally because Vite's dev server and vite preview already do that. In production you add one rewrite rule to your host: send every path that isn't a real file to /index.html.

  • heap limit

    Fix deploy errorsNov 3, 2026

    JavaScript heap out of memory during next build

    next build fails with "JavaScript heap out of memory" when the Node.js process running the build fills V8's old-space heap. The heap has a size limit of its own, separate from how much RAM the machine has. The quick fix is to raise that limit for the build only, with --max-old-space-size set below the memory the build machine actually has. The lasting fix is to find which build step uses the memory (bundling, TypeScript or static page generation) and turn down that step: source maps, the type check, or webpack's cache.

  • VITE_*

    Fix deploy errorsOct 29, 2026

    Vite env variables undefined in production: causes and fixes

    A Vite env variable is undefined in production when the build never saw it. Vite replaces every import.meta.env.VITE_ reference with a fixed value while vite build runs, so a variable added to your server afterwards changes nothing until you build again. The other common causes are a name without the VITE_ prefix and code that reads process.env in the browser.

EthioDeploy

Agentic first development and deployment, started in Ethiopia, built for developers everywhere.

Telegram community
Product
PricingAI coding agentAgent mode docsDomainsCLIFree backend hostingHosting in EthiopiaChangelog
Deploy guides
Next.jsNode.jsPythonSvelteKitTanStack StartDjangoFastAPIFlaskExpressNestJSLaravelRailsGoTelegram botLovableBoltReplitv0Google AI StudioBase44Firebase StudioClaude Code & Cursor
Compare
vs Herokuvs Vercelvs Rendervs Railwayvs Fly.iovs NetlifyHeroku alternativesVercel alternativesNetlify alternativesRender alternativesRailway alternativesFly.io alternativesReplit alternatives
Resources
DocsBlogFree toolsAboutSupportContactllms.txt
© 2026 EthioDeployPay by card in USD or bank transfer in ETBTermsPrivacy