Unlocking Node.js Child Process Communication with NODE_CHANNEL_FD
This article explains how Node.js uses the NODE_CHANNEL_FD environment variable to establish a socketpair for IPC between a parent process and its child, detailing the spawn implementation, underlying libuv handling, and how a Go subprocess can read and write through the shared file descriptor.
Node.js Communication with Child Processes
In the official Node.js documentation a description mentions that a child process can obtain a file descriptor for communication with its parent via the NODE_CHANNEL_FD environment variable. This article explores where NODE_CHANNEL_FD comes from and how to use it.
First, the child_process.spawn method is used to start a subprocess. The following example launches a Go program:
const { spawn } = require('child_process');
const { join } = require('path');
const childProcess = spawn('go', ['run', 'main.go'], {
stdio: [0, 1, 2, 'ipc']
});In the stdio array the string ipc signals that an inter‑process communication channel should be created. Node.js processes this option internally, converting it into a pipe and assigning a file descriptor index ( ipcFd) that later becomes NODE_CHANNEL_FD:
// https://github.com/nodejs/node/blob/7b1e15353062feaa3f29f4fe53e11a1bc644e63c/lib/internal/child_process.js#L1025-L1043
stdio = ArrayPrototypeReduce(stdio, (acc, stdio, i) => {
if (stdio === 'ignore') {
// ignore
} else if (stdio === 'ipc') {
ipc = new Pipe(PipeConstants.IPC);
ipcFd = i;
ArrayPrototypePush(acc, {
type: 'pipe',
handle: ipc,
ipc: true
});
} else if (stdio === 'inherit') {
// ignore
}
return acc;
}, []);The index of the ipc entry (in this case 3) becomes the first file descriptor passed to the child process. The low‑level libuv implementation creates a socketpair, marks the descriptor with FD_CLOEXEC where appropriate, and then forks the child:
// https://github.com/nodejs/node/blob/f313b39d8f282f16d36fe99372e919c37864721c/deps/uv/src/unix/process.c#L789
static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
int stdio_count,
int (*pipes)[2],
int error_fd,
pid_t* pid) {
*pid = fork();
if (*pid == 0) {
uv__process_child_init(options, stdio_count, pipes, error_fd);
abort();
}
if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
abort();
if (*pid == -1)
return UV__ERR(errno);
return 0;
}During the child‑process initialization the file descriptor corresponding to the socketpair is duplicated onto descriptor 3 (the value of NODE_CHANNEL_FD) so that the child can read and write through it:
// stdio_count is 4, corresponding to [0,1,2,'ipc']
for (fd = 0; fd < stdio_count; fd++) {
close_fd = -1;
// when fd == 3, use_fd is the socketpair descriptor, e.g., 24
use_fd = pipes[fd][2];
if (fd == use_fd) {
// already correct
} else {
fd = dup2(use_fd, fd);
}
// ...
}Because the socket descriptor is not marked FD_CLOEXEC, it remains open after execvp runs the target program, allowing the child (in this case a Go process) to communicate with the Node.js parent.
Golang Process Communicating with a Node.js Parent
In Go the environment variable NODE_CHANNEL_FD provides the numeric file descriptor of the socket. The following code obtains a *os.File for that descriptor:
nodeChannelFD := os.Getenv(NODE_CHANNEL_FD)
nodeChannelFDInt, _ := strconv.Atoi(nodeChannelFD)
fd := os.NewFile(uintptr(int(nodeChannelFDInt)), "lbipc"+nodeChannelFD)Using the syscall package, Go can send and receive messages over the socket with Sendmsg and Recvmsg:
// Sending data
type Message struct {
Id string `json:"id"`
MsgType string `json:"type"`
Data string `json:"data"`
}
fdHandler := int(fd.Fd())
responseMsg := Message{Id: "id:1", Data: "hello world", MsgType: "test"}
jsonData, _ := json.Marshal(responseMsg)
syscall.Sendmsg(fdHandler, append(jsonData, '
'), nil, nil, 0) // Receiving data
fdHandler := int(fd.Fd())
syscall.Recvmsg(fdHandler, dataBuf, attachedDataBuf, 0)The complete implementation can be found in the midwayjs/lb repository.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
