Get started Dashboard
Troubleshooting ·

Fix Render “Port scan timeout reached, no open ports detected”

Explore with AI

Port scan timeout reached, no open ports detected. Bind your service to at least one port. If you don't need to receive traffic on any port, create a background worker instead.

Render's deploy waits for your web service to open a port on host 0.0.0.0, and fails the deploy when it never sees one. Usually the server is bound to localhost or 127.0.0.1, ignores the PORT variable (10000 by default), or crashes or hangs before it listens. Bind to 0.0.0.0:$PORT, or deploy code that takes no traffic as a background worker.

On this page

The build log is green, the start command runs, and then the deploy just waits. Lines like ==> No open ports detected, continuing to scan... repeat in the log. Eventually the deploy gives up with “Port scan timeout reached, no open ports detected” and is marked as failed. If an earlier deploy succeeded, it keeps serving traffic. If this is the first deploy, the service never goes live.

Render is telling you it started your process and never saw it accept connections. The message also suggests a way out: if the service doesn’t need incoming traffic, it shouldn’t be a web service at all.

What “no open ports detected” means on Render

A Render web service is reachable at its onrender.com subdomain, but your process isn’t exposed to the internet directly. Render’s load balancer receives each request and forwards it to your service on one port. The web services docs set the contract: every web service must bind to a port on host 0.0.0.0 to serve HTTP requests, and the default expected port is 10000. If Render fails to detect a bound port, the deploy fails and shows an error in the logs.

Render’s boot and port binding tutorial describes how a deploy goes live. The build succeeds, Render runs your start command, and a port scanner waits for a TCP listener on 0.0.0.0. Only when it finds one does Render move on to health checks and route traffic to the new deploy. While it waits, it logs the “continuing to scan” line. When it stops waiting, the deploy fails with the message above.

graph TB
    A[Build succeeds] --> B[Render runs the start command]
    B --> C{Listener on 0.0.0.0?}
    C -->|not yet| D[No open ports, keep scanning]
    D --> C
    D -->|scan gives up| E[Port scan timeout, deploy fails]
    C -->|yes| F[Detected service running on port]
    F --> G[Health checks, then live]
    style E fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
    style G fill:#d1fae5,stroke:#6ee7b7,color:#065f46

The tutorial also explains why a server that works on your laptop can fail here. Render’s port scanner runs in a separate network namespace from your process. A server listening only on a loopback address is invisible to it.

Read the runtime log before changing anything

Open the failed deploy in the dashboard and read the lines between ==> Running '<your start command>' and the timeout. You can also stream them with the Render CLI:

render logs --resources srv-abc123 --tail

Look for three things:

  1. Does your app print a listen line at all? No output after the start command points at a crash, a hang or a Docker image with nothing to run.
  2. Which address does it name? localhost, 127.0.0.1 or [::1] is a loopback bind.
  3. Which port does it name? Compare it with PORT, which is 10000 unless you’ve set it yourself.

The tutorial gives a quick way to read the listen line:

  • Server listening on http://localhost:3000: loopback and a port other than PORT, so both are wrong.
  • Server listening on http://0.0.0.0:3000: right interface, port other than PORT.
  • Server listening on http://localhost:10000: right port, loopback interface.

Then match your case to one of the causes below. They’re ordered by how often people hit them.

Cause 1: the server binds to localhost or 127.0.0.1

How to tell it’s yours: your app logs a listen line, and the address in it is localhost, 127.0.0.1 or the IPv6 loopback [::1]. A typical log:

==> Running 'npm start'
Server listening on http://localhost:3000
==> No open ports detected, continuing to scan...
==> No open ports detected, continuing to scan...

When Render can see the port on loopback, the timeout line names it. Deploy logs posted in GitHub issues show this form:

Port scan timeout reached, no open ports detected on 0.0.0.0. Detected open ports on localhost -- did you mean to bind one of these to 0.0.0.0?

Render’s tutorial names the usual sources: the Rails default, some Node servers in dev mode, and some Go HTTP libraries. It calls out an IPv6 version too: Puma can print Listening on tcp://[::1]:3000, which the scanner doesn’t see.

Fix: bind to 0.0.0.0 on PORT. Use the idiom for your server:

  1. Node with Express. Read PORT and pass the host:
    const express = require("express");
    const app = express();
    
    const port = process.env.PORT || 10000;
    
    app.listen(port, "0.0.0.0", () => {
      console.log(`Listening on 0.0.0.0:${port}`);
    });
  2. Python with gunicorn. On Render’s native Python runtime, the default environment variables include GUNICORN_CMD_ARGS set to --preload --access-logfile - --bind=0.0.0.0:10000. A plain start command such as gunicorn myapp.wsgi binds correctly. If you set the bind yourself, in a flag or in gunicorn.conf.py, keep it on all interfaces:
    gunicorn myproject.wsgi -b 0.0.0.0:$PORT
  3. Python with uvicorn. Render’s FastAPI guide uses this start command:
    uvicorn main:app --host 0.0.0.0 --port $PORT
  4. Flask’s built-in server. Pass the host and port explicitly:
    import os
    
    port = int(os.environ.get("PORT", 10000))
    app.run(host="0.0.0.0", port=port)
  5. Rails with Puma. Bind in the start command:
    bundle exec rails server -b 0.0.0.0 -p $PORT
  6. Go with net/http. Give the full address, with the host:
    port := os.Getenv("PORT")
    if port == "" {
    	port = "10000"
    }
    log.Fatal(http.ListenAndServe("0.0.0.0:"+port, nil))

Before pushing, run the same start command locally with PORT=10000 and check the listen line names 0.0.0.0:10000.

Cause 2: the server ignores PORT or picks a reserved port

How to tell it’s yours: the listen line shows 0.0.0.0, but the port is hardcoded, or it’s one of Render’s reserved ports.

The web services docs recommend binding your HTTP server to the port in PORT. Its default value is 10000 for all web services, and you can override it in the Render Dashboard. Render says that if you bind to a different port, it is usually able to detect and use it. Usually isn’t always, and the tutorial’s examples show a hardcoded 3000 among the failures. Binding to PORT removes the guesswork.

Three ports are reserved by Render and can’t be used: 18012, 18013 and 19099.

Watch out for a service that opens several listeners. Render forwards public traffic to only one HTTP port per web service. The tutorial shows the log for that case:

==> Detected service running on port 10000
==> Detected service running on port 8080

That isn’t a failure on its own, but make sure the public HTTP server is the one on PORT.

Fix: let PORT drive the port.

  1. Remove hardcoded ports from code and start commands. Read PORT instead, as in the snippets above.
  2. If you need a specific port, set the PORT environment variable on the service to that number. Render’s environment variables reference describes PORT as the port your HTTP server binds to.
  3. Move any listener off 18012, 18013 and 19099.
  4. Keep extra listeners, such as a metrics or admin endpoint, on other ports, and keep the public server on PORT.

Cause 3: the app crashes or hangs before it listens

How to tell it’s yours: after ==> Running '...' the log shows a stack trace, a missing environment variable, or a database error, then no listen line. Or the log shows output from something that never finishes, such as a migration or a console.

The tutorial gives two examples of a start command that never reaches the server:

==> Running 'rails console'
Loading production environment (Rails 7.1.0)
==> No open ports detected, continuing to scan...
==> Running 'rails db:migrate && rails server'
==> Migrating database
==> No open ports detected, continuing to scan...

In the first, the start command is an interactive console that waits for input forever. In the second, the server only starts after the migration, so a slow migration holds the port closed.

Fix: start the server, and only the server.

  1. Fix the error at the end of the log. Render’s troubleshooting guide lists the usual ones: missing environment variables, an invalid start command, and missing modules.
  2. Move one-off work into the pre-deploy command. The deploys docs say it runs after the build and before the start command, on a separate instance from your running service. In a Blueprint:
    services:
      - type: web
        name: api
        runtime: ruby
        buildCommand: bundle install
        preDeployCommand: bundle exec rails db:migrate
        startCommand: bundle exec rails server -b 0.0.0.0 -p $PORT
    The pre-deploy command is available for paid web services, private services and background workers.
  3. Make the start command a long-running server. A REPL, a one-off script or date will never open a port.

Cause 4: a worker is deployed as a web service

How to tell it’s yours: the code is a queue consumer, a bot, a scheduler or another loop that never serves HTTP. It runs fine, logs its own work, and the deploy still fails the port scan.

The message names this case itself: if you don’t need to receive traffic on any port, create a background worker instead. Render’s background workers docs describe them as services that run continuously, like a web service or a private service, but don’t receive any incoming network traffic. They usually poll a task queue, with Celery, Sidekiq, BullMQ, Asynq or similar.

The private services docs draw the line between the two internal types:

  • If the service binds to at least one port and receives private network traffic, create a private service. It isn’t reachable from the public internet and gets no onrender.com subdomain.
  • Otherwise, create a background worker. Workers can make network requests but can’t receive them.

graph TB
    A[Does it accept connections?] -->|no| B[Background worker]
    A -->|yes| C{From the public internet?}
    C -->|yes| D[Web service on 0.0.0.0 PORT]
    C -->|no| E[Private service]
    style B fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style D fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style E fill:#d1fae5,stroke:#6ee7b7,color:#065f46

Fix: deploy it as the right service type.

  1. Create a new Background Worker in the dashboard from the same repo and branch, with the same build and start commands and environment variables.
  2. Or declare it in your Blueprint with type: worker:
    services:
      - type: worker
        name: queue-worker
        runtime: python
        buildCommand: pip install -r requirements.txt
        startCommand: celery -A tasks worker --loglevel=info
  3. If other services call it over the private network, use type: pserv and bind it to at least one port.
  4. Once the new service is healthy, delete the old web service so it stops failing deploys.

Cause 5: the Docker image has nothing to run

How to tell it’s yours: the service uses the Docker runtime, the image builds, and then there’s no application output at all.

The troubleshooting guide says a Dockerfile must include a CMD or ENTRYPOINT directive, because Render uses one of them to run your app. Without both, the deploy might appear to hang indefinitely. The Docker docs also describe a Docker Command field under Advanced that replaces the Dockerfile’s CMD. A stale or wrong value there runs the wrong thing, or nothing that listens.

Fix: give the container a server to start.

  1. Add a CMD that starts the server on 0.0.0.0 and PORT. The exec form passes signals to your app:
    FROM node:20-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    CMD ["node", "server.js"]
  2. Check the Docker Command field in the service settings. Leave it empty to use the Dockerfile’s CMD, or set the full command. For several commands, Render’s docs pass them to /bin/sh -c:
    /bin/sh -c python manage.py migrate && gunicorn myapp.wsgi:application --bind 0.0.0.0:10000
  3. In a Blueprint, the same setting is dockerCommand, which defaults to the Dockerfile’s CMD.

Confirming Render found the port

Redeploy and read the log again. Render’s tutorial shows the lines you want after the fix:

==> Detected service running on port 10000
==> Your service is live 🎉

Then call the service from outside:

curl -sS -o /dev/null -w "%{http_code}\n" https://your-service.onrender.com/

A 200 (or whatever your root route returns) means the load balancer can reach your process. If the deploy now finds the port but still fails, the problem has moved to health checks. Render’s health checks docs say a default TCP check passes when the instance accepts the connection within five seconds, and an HTTP check passes on a 2xx or 3xx response within five seconds.

Stopping the port scan from failing again

  • Bind to 0.0.0.0:$PORT everywhere. Put it in the code or the start command, never in a local-only config file.
  • Test the exact start command locally with PORT=10000, and check the listen line before you push.
  • Keep the start command to one long-running server. Migrations go in the pre-deploy command.
  • Pick the service type on purpose. Web services for public HTTP, private services for internal traffic, background workers for everything that only makes outbound calls.
  • Turn on deploy failure notifications. Render’s notifications can email you or post to Slack when a build or deploy fails, so a failed port scan on an auto-deploy doesn’t go unnoticed.

Finding the failed port scan with Polylane

Once you connect a Render account, Polylane queries its logs and metrics directly and turns Render alerts into issues. It investigates each one and cites the log line behind every claim, such as the listen line that names 127.0.0.1.

Running on Render? See how Polylane monitors Render in production.

Common questions.

What port does Render expect a web service to use?

Render sets PORT to 10000 for every web service by default, and you can override it as an environment variable in the dashboard. Your server should read PORT and bind to host 0.0.0.0. Render is usually able to detect a different port, but binding to PORT is the documented recommendation.

Why does my app say it's listening but Render finds no open port?

It is almost always listening on a loopback address such as localhost, 127.0.0.1 or [::1]. Render's tutorial explains that its port scanner runs in a separate network namespace from your process, so a loopback listener is invisible to it. Change the bind address to 0.0.0.0 and redeploy.

Which ports can't I use on Render?

Render reserves ports 18012, 18013 and 19099, and they can't be used. A service can have at most 75 open ports. Render forwards public traffic to only one HTTP port per web service, so bind the public server to PORT and use any other ports for private network traffic.

My service doesn't serve HTTP. How do I stop this error?

Deploy it as a different service type. Render's docs say a background worker runs continuously but receives no incoming network traffic, which suits queue consumers such as Celery, Sidekiq or BullMQ. If other services need to call it over the private network, create a private service, which must still bind to at least one port.

Does gunicorn on Render bind to 0.0.0.0 by default?

On Render's native Python runtime, Render sets GUNICORN_CMD_ARGS to --preload --access-logfile - --bind=0.0.0.0:10000, so a plain gunicorn start command binds correctly. If you pass your own --bind, make it 0.0.0.0:$PORT. A bind to 127.0.0.1 in a config file or flag leads straight to this error.

Can a health check cause “no open ports detected”?

No. The port scan happens before health checks matter, and Render's tutorial shows the scanner waiting for a TCP listener first. Once a port is found, a failing HTTP health check is a different problem. Render counts an HTTP check as passing when it gets a 2xx or 3xx response within five seconds.

How do I run migrations without blocking the port scan?

Move them into the pre-deploy command, which runs after the build and before the start command on a separate instance. The start command should then only start the server. The pre-deploy command is available for paid web services, private services and background workers.

Sources

  1. Web Services: port binding (Render Docs)
  2. Background Workers (Render Docs)
  3. Private Services (Render Docs)
  4. Private Network: port restrictions (Render Docs)
  5. Default Environment Variables (Render Docs)
  6. Docker on Render (Render Docs)
  7. Deploying on Render: deploy steps (Render Docs)
  8. Health Checks (Render Docs)
  9. Troubleshooting Your Deploy (Render Docs)
  10. Deploy a FastAPI App (Render Docs)
  11. Your First Render Deploy (Render Docs)
  12. Render CLI reference (Render Docs)
  13. Blueprint YAML reference (Render Docs)
  14. Notifications (Render Docs)
  15. Boot and port binding (Render Tutorials)
  16. Polylane documentation
  17. Polylane full content

About the author

Boris Tane

Founder of Polylane

Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.

Related

Nobody should be on-call. Polylane watches your infra, finds what broke, and writes the fix.

Get started for free