# NuProcess: non-blocking process pipes for Java without the pumper threads

> NuProcess is a Java implementation of external process execution that uses the Foreign Function and Memory API to drive the platform's own readiness notification mechanism, epoll, kqueue or IO Completion Ports, so a spawned process's pipes are handled without a thread per stream. It suits JVM services that start many child processes, and the honest headline is memory rather than speed, since the README says there is no significant speed difference over java.lang.Process.

**brettwooldridge/NuProcess** — Low-overhead, non-blocking I/O, external Process implementation for Java

- Repository: https://github.com/brettwooldridge/NuProcess
- Stars: 741 · Forks: 82
- Language: Java
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/brettwooldridge-nuprocess

## The pumper thread tax on java.lang.Process

NuProcess is a low-overhead, non-blocking I/O, external process execution implementation for Java, and it is a replacement for `java.lang.ProcessBuilder` and `java.lang.Process`.

The problem it addresses is stated in the README as a question. Have you ever been annoyed by the fact that whenever you spawn a process in Java you have to create two or three pumper threads, for every process, to pull data out of the stdout and stderr pipes and pump data into stdin? The answer that follows is that if your code starts a lot of processes you can have dozens or hundreds of threads doing nothing but pumping data.

That is the real cost. The standard library's process support is thread based at the pipe level, so the concurrency of your application is paid for in threads, and each thread carries a stack and a scheduler slot. A service that runs a few processes does not notice. A build tool, a test harness or a container supervisor that runs hundreds at once does.

NuProcess takes the opposite approach. It uses the Java Foreign Function and Memory, or FFM, API to invoke platform-specific native APIs to achieve non-blocking I/O on the pipes between your Java process and the spawned processes. The list is per platform: Linux uses epoll, MacOS X uses kqueue and kevent, and Windows uses IO Completion Ports. All three are the operating system's own readiness notification mechanism, so the same Java code gets the right primitive on each platform rather than a portable emulation of one.

The project is by Brett Wooldridge, published on Maven Central as `com.zaxxer:nuprocess`, and licensed under Apache-2.0.

## The gain is memory, not speed, and the README says so plainly

The section titled It's mostly about the memory is the most useful paragraph in the README, and it is unusual for a project of this kind to state its own performance case that plainly.

On speed, the README says there is not a significant difference between NuProcess and the standard Java Process class, even when running 500 concurrent processes. On some platforms such as MacOS X or Linux, it adds, NuProcess is 20 percent faster than `java.lang.Process` for large numbers of processes. So the speed case is a rounding error, and a vendor claiming otherwise would not be believable.

On memory, the difference is significant, and the reason is fork. On unix-based platforms such as Linux, when creating a new process `java.lang.Process` uses a fork and exec operation, and that requires a temporary copy of the Java process, the fork, before the exec is performed. The README puts a number on it: when running tests on Linux, in order to spawn 500 processes it required setting the JVM max memory to 3Gb with `-Xmx3g`.

NuProcess uses a variant of fork called vfork, which does not impose this overhead, and the README states it can comfortably spawn 500 processes even when running the JVM with only 128Mb. So the headline saving is a JVM that does not have to be given three gigabytes of heap in order to start a few hundred children.

That distinction between the two costs is the whole argument for the project. If your problem is latency of a single process, the standard library is fine and NuProcess adds a native layer for nothing. If your problem is a process-per-task workload and the memory to spawn them, the argument is real and quantified.

## Two process APIs in one library, and the version split that decides which you get

NuProcess ships two distinct implementations under one name, and the split is decided by the Java version you run.

The 4.x line uses the Foreign Function and Memory API and requires Java 25 or newer. The 3.x line and earlier are based on JNA and support Java 1.7 and above. Those are very different foundations. FFM is a first-party Java API for calling native code, with the library's own model of downcall handles and memory segments. JNA is a third-party library that maps native calls onto Java interfaces, which is what made a JNA-based NuProcess possible on Java 1.7 back when that was a reasonable floor.

The Maven coordinates for the current line are:

```xml
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>nuprocess</artifactId>
        <version>4.0.0</version>
        <scope>compile</scope>
    </dependency>

```

and for a Java version prior to 25, the JNA-based 3.x release:

```xml
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>nuprocess</artifactId>
        <version>3.0.0</version>
        <scope>compile</scope>
    </dependency>

```

The README also carries a breaking change note: versions 1.0.0 and above contain breaking API changes from 0.9.7. That is a long time ago now, but it matters if you find an old example online, because the handler interface and the builder API have not been stable since that point.

There are no GitHub releases for the project, so Maven Central is the version reference. The last push was on 2026-07-10 and the repository is not archived, so work continues, but the versioning story on GitHub is a repository with a `pom.xml`, a `src/` directory, a `benchmark/` directory and CI configuration files for CircleCI, Travis and AppVeyor, several of which are older CI systems that suggest the project's active period.

`CHANGES.md` at the root is the place to check what changed between versions.

## NuProcessBuilder, ProcessHandler and the wantWrite handshake

The API mirrors `ProcessBuilder`, so the entry point is familiar. The README's example starts the Unix cat command, writes to its stdin and reads the echoed reply from stdout.

```java
NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("/bin/cat"));
ProcessHandler handler = new ProcessHandler();
pb.setProcessListener(handler);
NuProcess process = pb.start();
process.wantWrite();
process.waitFor(0, TimeUnit.SECONDS); // when 0 is used for waitFor() the wait is infinite

```

The `ProcessHandler` in that code is a class you provide, which receives callbacks from the process to handle input, output and termination. The `wantWrite()` call is the part with a rule attached to it: it expresses that there is something to write to the process, so the handler will be called back to perform the write. Without it, the write callback never fires.

The handler looks like this:

```java
class ProcessHandler extends NuAbstractProcessHandler {
   private NuProcess nuProcess;

   @Override
   public void onStart(NuProcess nuProcess) {
      this.nuProcess = nuProcess;
   }
   
   @Override
   public boolean onStdinReady(ByteBuffer buffer) {
      buffer.put("Hello world!".getBytes());
      buffer.flip();
      return false; // false means we have nothing else to write at this time
   }

   @Override
   public void onStdout(ByteBuffer buffer, boolean closed) {
      if (!closed) {
         byte[] bytes = new byte[buffer.remaining()];
         // You must update buffer.position() before returning (either implicitly,
         // like this, or explicitly) to indicate how many bytes your handler has consumed.
         buffer.get(bytes);
         System.out.println(new String(bytes));

         // For this example, we're done, so closing STDIN will cause the "cat" process to exit
         nuProcess.closeStdin(true);
      }
   }
}

```

Three details in that code are contracts rather than style. `onStdinReady` returns a boolean, and false means there is nothing else to write at this time, which is how you tell the library you are done for now. `onStdout` receives a `ByteBuffer` and a closed flag, and you must update `buffer.position()` before returning, either implicitly by reading or explicitly, to indicate how many bytes your handler has consumed. And `closeStdin(true)` closes stdin, which in the example is what causes cat to exit.

That buffer position rule is the one that bites people. Forget it and the library cannot tell how much of the data you consumed, so the next callback arrives with a buffer position that does not match what you read.

## Synchronous writes that are non-blocking underneath

NuProcess also allows synchronous writes to the stdin of the spawned process. The README is precise about what that means: even though the writes are synchronous they are non-blocking, meaning the write returns immediately.

The API differences follow from that. In this model you do not use `NuProcess.wantWrite()` and your `onStdinReady()` method will not be called. If you extend `NuAbstractProcessHandler` you do not need to provide an implementation of `onStdinReady()`. You use the `NuProcess.writeStdin()` method instead, which returns immediately, and writes are queued and occur in order.

```java
NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("/bin/cat"));
ProcessHandler handler = new ProcessHandler();
pb.setProcessListener(handler);
NuProcess process = pb.start();

ByteBuffer buffer = ByteBuffer.wrap("Hello, World!".getBytes());
process.writeStdin(buffer);

process.waitFor(0, TimeUnit.SECONDS); // when 0 is used for waitFor() the wait is infinite

```

The handler for that version drops the stdin callback entirely:

```java
class ProcessHandler extends NuAbstractProcessHandler {
   private NuProcess nuProcess;

   @Override
   public void onStart(NuProcess nuProcess) {
      this.nuProcess = nuProcess;
   }
   
   public void onStdout(ByteBuffer buffer, boolean closed) {
      if (!closed) {
         byte[] bytes = new byte[buffer.remaining()];
         // You must update buffer.position() before returning (either implicitly,
         // like this, or explicitly) to indicate how many bytes your handler has consumed.
         buffer.get(bytes);
         System.out.println(new String(bytes));

         // For this example, we're done, so closing STDIN will cause the "cat" process to exit
         nuProcess.closeStdin(true);
      }
   }
}

```

Two cautions are worth carrying over. First, the writes are ordered and queued, which means the call returning successfully tells you the data was accepted for writing, not that it reached the child process. Second, the README explicitly directs you to the JavaDoc for the `NuProcess.writeStdin()` method for cautions and caveats, which tells you the author considers the details of this path worth reading rather than guessing.

The JavaDoc for the `NuProcessHandler` interface is called out as the primary contract with NuProcess, and the documentation warns you to read and fully understand it. That is the right advice for a callback-based API where the sequencing rules matter.

## Where the standard library is the better answer

The comparison worth making is not with another process library but with the code you already have.

If you start one or two processes, use `java.lang.ProcessBuilder`. The standard library is in the JDK, it has no native dependency, it works on every JVM vendor's build, and its API is one your colleagues already know. The thread cost of a couple of pumper threads is invisible.

If you start hundreds at once, the picture inverts. The README's own measurement, 500 processes needing `-Xmx3g` under the standard library against 128Mb with NuProcess, is the whole argument. The same workload with the standard library also puts you in a position where the fork of a multi-gigabyte heap is a real cost on a memory constrained container, and a pumper thread per stream per process is a scheduling cost on a busy machine.

There is a second alternative for that case, which is to not use processes. A Java service that needs to run a hundred external tools in parallel might be better served by an in-process implementation of what the tools do, or by a job runner that does the spawning in a separate process from the JVM. Neither is free: reimplementing a tool's behaviour in Java is a maintenance commitment, and an external runner means giving up in-process control of the child's lifetime.

The remaining consideration is the platform dependency. NuProcess 4.x calls into epoll, kqueue or IO Completion Ports, and it does so through the FFM API, which is still a young Java API relative to the maturity of the library itself. The 3.x line, built on JNA, is the version that works on Java 1.7 and above, and it is the one to choose if your runtime is not current.

For anything in between, the decision comes down to whether your process count is a constant you have thought about or an accident of your architecture.

## Conclusion

Adopt NuProcess when a JVM service spawns many child processes and the thread count and the memory that fork costs are the problem, and use the 4.x line only if you are on Java 25 or newer, since it requires the Foreign Function and Memory API. Do not adopt it for speed alone, because the README states there is no significant performance difference over java.lang.Process, and check first whether the synchronous write model with its queued writes suits you, since writes are ordered but fire and forget and you will not know a write failed from the call alone.

## FAQ

### What problem does NuProcess solve?

It replaces java.lang.ProcessBuilder and java.lang.Process for applications that spawn many child processes, avoiding the two or three pumper threads per process the standard library needs for stdout, stderr and stdin. It uses the Foreign Function and Memory API to drive epoll on Linux, kqueue and kevent on MacOS X, and IO Completion Ports on Windows.

### Is NuProcess faster than java.lang.Process?

Not significantly, according to the README. It says there is no significant speed difference even with 500 concurrent processes, and that on MacOS X or Linux it is 20 percent faster for large numbers of processes. The meaningful difference is memory, since NuProcess uses vfork and can spawn 500 processes with the JVM at 128Mb rather than needing -Xmx3g.

### Which Java version does NuProcess 4.x need?

Java 25 or newer, because the 4.x line is based on the Foreign Function and Memory API. Versions 3.x and earlier are based on JNA and support Java 1.7 and above, so the 3.0.0 release is the one to use on an older runtime.

### How do I write to the stdin of a NuProcess?

There are two ways. In the asynchronous model you call NuProcess.wantWrite() so the handler's onStdinReady callback fires, returning false when you have nothing more to write. In the synchronous model you call process.writeStdin(buffer) directly; the write returns immediately, writes are queued and occur in order, and onStdinReady is never called.

### What has to be done in the onStdout callback?

You must update buffer.position() before returning, either implicitly by reading the bytes or explicitly, to indicate how many bytes your handler has consumed. The callback also receives a closed flag so you can tell a finished stream from an open one.

### Is NuProcess still being maintained?

The repository is not archived and the last push was on 2026-07-10. There are no GitHub releases, so Maven Central holds the version reference, with 4.0.0 for the current line and 3.0.0 for the JNA-based line, and the code is licensed under Apache-2.0.

## Sources

- [brettwooldridge/NuProcess on GitHub](https://github.com/brettwooldridge/NuProcess)
- [Issues](https://github.com/brettwooldridge/NuProcess/issues)
- [License: Apache-2.0](https://github.com/brettwooldridge/NuProcess/blob/master/LICENSE)
- [README](https://github.com/brettwooldridge/NuProcess/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/brettwooldridge-nuprocess
