n8n-io/n8n · error · OSError

Port {config.port} is already in use

Error message

Port {config.port} is already in use

What it means

Thrown by HealthCheckServer.start when asyncio.start_server fails with EADDRINUSE, meaning the configured health check port is already bound by another process. The original OSError is chained (from e) so the traceback is preserved, but the message is made more user-friendly by naming the specific port.

Source

Thrown at packages/@n8n/task-runner-python/src/health_check_server.py:29

class HealthCheckServer:
    def __init__(self):
        self.server: asyncio.Server | None = None
        self.logger = logging.getLogger(__name__)

    async def start(self, config: HealthCheckConfig) -> None:
        try:
            self.server = await asyncio.start_server(
                self._handle_request, config.host, config.port
            )
            # for OS-assigned port in tests
            actual_port = self.server.sockets[0].getsockname()[1]
            self.logger.info(
                f"Health check server listening on {config.host}, port {actual_port}"
            )
        except OSError as e:
            if e.errno == errno.EADDRINUSE:
                raise OSError(f"Port {config.port} is already in use") from e
            else:
                raise

    async def stop(self) -> None:
        if self.server:
            self.server.close()
            await self.server.wait_closed()
            self.server = None
            self.logger.info("Health check server stopped")

    async def _handle_request(
        self, _reader: asyncio.StreamReader, writer: asyncio.StreamWriter
    ) -> None:
        try:
            writer.write(HEALTH_CHECK_RESPONSE)
            await writer.drain()
        except Exception:
            pass

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Find and stop the process holding the port: use 'lsof -i :<port>' or 'ss -tlnp | grep <port>'.
  2. Change N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT to a different available port.
  3. Set the port to 0 to let the OS assign an available port automatically (useful in tests).
  4. Ensure previous runner processes are fully terminated before starting new ones.

Example fix

# before
export N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT=8080
# another process is on 8080
# after — use a different port
export N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT=8081

# or find and kill the stale process
# lsof -i :8080  # then kill <PID>
Defensive patterns

Strategy: try-catch

Validate before calling

import socket

def is_port_available(host: str, port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        try:
            s.bind((host, port))
            return True
        except OSError:
            return False

if not is_port_available(config.host, config.port):
    raise RuntimeError(f'Port {config.port} is already in use')

Try / catch

from health_check_server import HealthCheckServer

try:
    await server.start(config)
except OSError as e:
    if 'already in use' in str(e):
        print(f'Port {config.port} is busy — try a different port or stop the conflicting process')
        config.port = 0  # let OS assign
        await server.start(config)

Prevention

When it happens

Trigger: The Python task runner attempts to bind its health check TCP server to config.port, but another process (another runner instance, a different service, or a stale runner process) is already listening on that port. asyncio.start_server raises OSError with errno EADDRINUSE.

Common situations: Running multiple runner instances on the same host with the same health check port. A previous runner process didn't shut down cleanly and is still holding the port. Another application happens to use the same port. Port conflict in a container orchestration environment where multiple pods share host networking.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/6c65ac71ed32302f. Report an issue: GitHub.