{"record":{"id":"98c37019176e1e91","repo":"denoland/deno","slug":"roundrobinhandle-add-worker-already-added","errorCode":null,"errorMessage":"RoundRobinHandle.add: worker already added","messagePattern":"RoundRobinHandle\\.add: worker already added","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/internal/cluster/round_robin_handle.ts","lineNumber":92,"sourceCode":"    this.handle.onconnection = (err: number, handle: any) =>\n      this.distribute(err, handle);\n    if (this.handle instanceof TCP) {\n      setupTCPListenWrap(this.handle);\n    } else if (this.handle instanceof Pipe) {\n      setupPipeListenWrap(this.handle);\n    }\n    this.server._handle = null;\n    this.server = null;\n  });\n}\n\nRoundRobinHandle.prototype.add = function (\n  this: any,\n  worker: any,\n  send: (errno: number | null, reply: any, handle: any) => void,\n) {\n  if (this.all.has(worker.id)) {\n    throw new Error(\"RoundRobinHandle.add: worker already added\");\n  }\n  this.all.set(worker.id, worker);\n\n  const done = () => {\n    if (this.handle.getsockname) {\n      const out: any = {};\n      this.handle.getsockname(out);\n      send(null, { sockname: out }, null);\n    } else {\n      send(null, null, null); // UNIX socket.\n    }\n\n    this.handoff(worker); // In case there are connections pending.\n  };\n\n  if (this.server === null) {\n    return done();\n  }","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/node/polyfills/internal/cluster/round_robin_handle.ts#L74-L110","documentation":"In cluster round-robin mode (the default off-Windows), the primary tracks which workers belong to each listening server in a RoundRobinHandle keyed by worker.id. add() throws a plain Error if that id is already registered — an internal invariant violation meaning the same worker was added to the same handle twice, which in practice means duplicated listening messages for one (address, port) query.","triggerScenarios":"A single cluster worker calling server.listen() twice on the same port (two listening messages, same query, same worker.id); custom re-fork logic that reuses worker ids while the old registration still exists in the primary.","commonSituations":"Starting two servers on the same port inside one worker under cluster; worker-restart scripts that fork replacements with explicit ids; migrations where the primary's handle bookkeeping desynced from reality.","solutions":["Listen once per port per worker; fork a fresh worker instead of re-listening inside the same one","Do not pass explicit ids when forking replacement workers; let cluster assign them","If it persists, return to stock cluster.fork() usage and remove custom setupPrimary hooks that touch internals"],"exampleFix":"// before (inside one worker)\nhttp.createServer(handler).listen(8080);\nhttp.createServer(handler2).listen(8080); // same worker + port -> duplicate add\n\n// after\nconst server = http.createServer(handler);\nserver.on(\"request\", handler2); // one listener per port per worker\nserver.listen(8080);","handlingStrategy":"validation","validationCode":"// one listen per (worker, port)\nconst listening = new Set();\nfunction listenOnce(server, port) {\n  if (listening.has(port)) {\n    throw new Error(`worker already listening on ${port}`);\n  }\n  listening.add(port);\n  server.listen(port);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Call listen() exactly once per port inside each cluster worker","Let cluster assign worker ids; never reuse ids across fork cycles","Avoid custom setupPrimary hooks that touch internal handle bookkeeping"],"tags":["cluster","load-balancing","internal-invariant"],"backgroundTag":"duplicate-worker-registration","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}