{"record":{"id":"1ce61756f8a9beb5","repo":"mudler/LocalAI","slug":"grpc-port-range-is-exhausted-reusing-a-port-that","errorCode":null,"errorMessage":"gRPC port range is exhausted; reusing a port that belonged to another backend. A stale controller row for the previous owner could briefly misroute to this backend — raise LOCALAI_GRPC_MAX_PORT to restore headroom","messagePattern":"gRPC port range is exhausted; reusing a port that belonged to another backend\\. A stale controller row for the previous owner could briefly misroute to this backend — raise LOCALAI_GRPC_MAX_PORT to restore headroom","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"core/services/worker/supervisor.go","lineNumber":300,"sourceCode":"\t\tif _, owned := owners[port]; owned {\n\t\t\tcontinue\n\t\t}\n\t\ts.freePorts = slices.Delete(s.freePorts, i, i+1)\n\t\treturn s.claimPort(key, port), nil\n\t}\n\n\t// 3. Grow into ports never handed out, staying inside the range.\n\tif s.nextPort >= minPort && s.nextPort <= maxPort {\n\t\tport := s.nextPort\n\t\ts.nextPort++\n\t\treturn s.claimPort(key, port), nil\n\t}\n\n\t// 4. Steal another key's port rather than refuse to start a backend.\n\tif len(s.freePorts) > 0 {\n\t\tport := s.freePorts[len(s.freePorts)-1]\n\t\ts.freePorts = s.freePorts[:len(s.freePorts)-1]\n\t\txlog.Warn(\"gRPC port range is exhausted; reusing a port that belonged to another backend. A stale controller row for the previous owner could briefly misroute to this backend — raise LOCALAI_GRPC_MAX_PORT to restore headroom\",\n\t\t\t\"backend\", key, \"port\", port, \"previousOwner\", owners[port], \"min\", minPort, \"max\", maxPort)\n\t\treturn s.claimPort(key, port), nil\n\t}\n\n\treturn 0, fmt.Errorf(\"%w: %d-%d is fully consumed by %d running backend(s) and %d port(s) still in quarantine; raise LOCALAI_GRPC_MAX_PORT to widen the range\",\n\t\tErrNoFreePort, minPort, maxPort, len(s.processes), len(s.quarantinedPorts))\n}\n\n// sweepAffinity drops claims whose window has lapsed, so their ports become\n// ordinary free ports again. Swept lazily on allocation for the same reason as\n// sweepQuarantine: the only observer is allocation itself, so a timer goroutine\n// per released port would buy nothing. Callers must hold s.mu.\nfunc (s *backendSupervisor) sweepAffinity() {\n\tif len(s.portAffinity) == 0 {\n\t\treturn\n\t}\n\tnow := time.Now()\n\tfor key, own := range s.portAffinity {","sourceCodeStart":282,"sourceCodeEnd":318,"githubUrl":"https://github.com/mudler/LocalAI/blob/44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26/core/services/worker/supervisor.go#L282-L318","documentation":"Warning emitted by the backend supervisor's port allocator when the LOCALAI_GRPC min-max gRPC port range has no never-handed-out ports left, so it pops a port from the free pool that previously belonged to a different backend and reassigns it. The danger called out in the message: if the controller still has a stale row pointing the old owner's address at this port, requests can briefly route to the wrong backend. Allocation still succeeds; this is a degraded-mode heads-up, not a failure.","triggerScenarios":"Running enough concurrent backends that s.nextPort walks past maxPort (step 3 fails) while freePorts is non-empty — e.g. several models loaded and one restarting — so the allocator takes step 4, steals freePorts[len-1], logs the warning with the previous owner, and claimPort()s it for the new key.","commonSituations":"Default LOCALAI_GRPC port range too small for the number of model shards/replicas; frequent backend restarts churning ports; single-box dev setups with many small models; misconfigured min/max env vars leaving only a handful of ports.","solutions":["Raise LOCALAI_GRPC_MAX_PORT (and keep LOCALAI_GRPC_MIN_PORT sane) so nextPort growth plus quarantine headroom covers your peak concurrent backend count.","Reduce simultaneous backend count: unload models you are not using or lower concurrency limits so fewer gRPC processes live at once.","If misrouting is suspected, restart the LocalAI process (or the affected backends) so controller rows and port ownership resynchronize, then re-issue requests.","Monitor this warning in logs and size the range at (max concurrent backends) + margin for quarantined ports."],"exampleFix":"# before\nexport LOCALAI_GRPC_MIN_PORT=50000\nexport LOCALAI_GRPC_MAX_PORT=50010   # 10 ports, exhausted with >10 backend churn\n\n# after\nexport LOCALAI_GRPC_MIN_PORT=50000\nexport LOCALAI_GRPC_MAX_PORT=50100   # headroom for concurrent backends + quarantine","handlingStrategy":"validation","validationCode":"# Before starting more backends, compare configured range size against\n# running backends + quarantine headroom (bash + localai admin API)\nMIN=${LOCALAI_GRPC_MIN_PORT:-50000}\nMAX=${LOCALAI_GRPC_MAX_PORT:-50100}\nRUNNING=$(curl -s localhost:8080/backends | jq 'length')\nif [ $((MAX - MIN + 1)) -lt $((RUNNING + 10)) ]; then\n  echo \"gRPC port headroom low; raise LOCALAI_GRPC_MAX_PORT\" >&2\nfi","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Size LOCALAI_GRPC range to peak concurrent backends plus a quarantine margin (e.g. 2x backends + 20).","Alert on the 'gRPC port range is exhausted' warning log so you widen the range before ErrNoFreePort failures appear.","Unload idle models to keep live backend count well under range capacity."],"tags":["go","grpc","ports","resource-exhaustion","supervisor","localai"],"backgroundTag":null,"analyzedSha":"44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26","analyzedAt":"2026-08-15T10:13:50.291Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}