# Node execFile was 2.7x slower: the spawn cost nothing

> Two implementations of the same memory lookup, one over a Unix socket and one shelling out to a CLI. I blamed the subprocess. The timing block said otherwise.

- Author: Chad Priest
- Published: 2026-08-26
- Canonical URL: https://blog.vodou.ai/node-execfile-was-2-7x-slower-the-spawn-cost-nothing/
- Tags: node, performance, debugging, mcp

---

Same machine, same daemon, same question. The gateway got its memory context in **1495ms**. The MCP server got it in **4041ms**. Both of them are code I wrote, in the same repo, for the same operation.

## Unix socket in llm.ts, execFile in vodou-memory/index.js:38

The gateway talks to the Rust daemon over a Unix socket, one round trip, in `MCP-servers/Vodou-Console/src/llm.ts`:

```ts
const request = JSON.stringify({ cmd: 'prompt', payload: { hook_json: ... } }) + '\n';
const client = net.createConnection({ path: sockConnectTarget(sockPath) }, () => {
  client.write(request);
  client.end();
});
```

The MCP server shells out to the CLI instead, in `MCP-servers/vodou-memory/index.js:38`:

<!-- REDACT-OK: this env var configures the Apache-2.0 CLI wrapper, already public on GitHub -->
```js
function core(args, timeoutMs = 20000) {
  return new Promise((resolve, reject) => {
    execFile(VODOU_CORE, args, { cwd: PROJECT_ROOT, timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 }, ...);
  });
}
```

<!-- REDACT-OK: this env var configures the Apache-2.0 CLI wrapper, already public on GitHub -->
`VODOU_CORE` is a 33MB Rust binary. So my diagnosis wrote itself: fork, exec, page in 33 megabytes, parse argv, tear it down. Of course that's slower. Delete the second implementation, point it at the socket, ship.

## Forking the 33MB binary measured zero milliseconds, twice

Before deleting anything I measured the thing I was about to blame:

```
$ /usr/bin/time -p ./vodou-core --help
real 0.00
```

Zero. Twice. Fork/exec of a 33MB binary on an M-series Mac is free at this resolution, and I'd have known that a year ago if I'd ever checked instead of assuming.

The real answer was already sitting in the JSON I'd captured, because the CLI reports its own timing:

```json
"timing": { "search_ms": 3998, "selected_ms": 0, "total_ms": 4041 }
```

3998 of 4041ms happened **inside the daemon**, on the other side of the socket. The subprocess accounted for 43ms of a 4-second call. The two implementations weren't a fast transport and a slow transport. They were two different questions asked of the same backend: `mem context` over-fetches to the 50-cap, resolves vault membership, then runs the cross-encoder rerank, and `cmd:'prompt'` does not.

Two other things fell out of measuring instead of theorizing. `mem context` without a vault fails closed and loud: `error: the following required arguments were not provided: --vault <VAULT>`. And an empty query dies loudly inside the engine with `empty query — pass a non-empty search string` rather than quietly returning the whole store.

## Instrument under both paths before deleting the execFile call

When you find two implementations of one operation and one is slower, the transport is the decoy. Instrument the layer *underneath* both before you delete either: the subprocess, the HTTP hop, the extra serialization are all things you can see, which is exactly why they attract blame.

If the shared backend already emits timing, read it before you write a benchmark. Mine did.

---

Source: [Node execFile was 2.7x slower: the spawn cost nothing](https://blog.vodou.ai/node-execfile-was-2-7x-slower-the-spawn-cost-nothing/) by Chad Priest, from Building Vodou in Public.
