rubyguides

Kernel#fork

fork -> integer or nil

The fork method creates a new process by duplicating the current process. The child process gets a copy of all memory, while the parent receives the child’s process ID.

Basic Usage

pid = fork do
  # This runs in child process
  puts "Child process: #{Process.pid}"
end

# This runs in parent
puts "Parent: #{Process.pid}, Child PID: #{pid}"

# Wait for child
Process.wait(pid)

Inside the block passed to fork, the return value is nil because that code runs in the child process. The parent receives the child’s process ID as the return value of fork itself. This dual-return pattern is how you distinguish parent from child: check whether the PID is nil to determine which branch of logic to follow after forking.

Practical Examples

Parallel Processing

tasks = [1, 2, 3, 4]

tasks.each do |task|
  fork do
    # Process task in parallel
    result = process(task)
    File.write("result_#{task}.txt", result)
  end
end

# Wait for all children
Process.waitall

Forking inside a loop creates independent processes that run concurrently, each with its own copy of the parent’s memory at the moment of the fork. Writing results to separate files is a simple way to collect output without needing inter-process communication, but it means the parent must read those files back after calling Process.waitall. For heavier workloads or when the child needs to stream results back continuously, pipes or sockets offer a more direct data path between parent and child.

Creating worker pool

def spawn_workers(count)
  count.times do |i|
    fork do
      loop do
        job = get_next_job
        process_job(job) if job
      end
    end
  end
end

A worker pool built with fork gives you a fixed number of long-lived processes that can handle many jobs without the overhead of forking per task. The parent becomes a supervisor that delegates work rather than doing work itself. If a worker exits unexpectedly, the supervisor can detect it with Process.wait and spawn a replacement, keeping the pool size steady across the lifetime of the application.

Daemon Process

def daemonize
  fork do
    Process.setsid  # New session
    fork do
      Dir.chdir("/")
      File.umask(0000)
      STDIN.reopen("/dev/null")
      STDOUT.reopen("/dev/null", "a")
      STDERR.reopen("/dev/null", "a")
      run_daemon
    end
    exit!
  end
  Process.waitall
end

The double-fork daemon pattern has a specific purpose: the first fork detaches from the terminal session, and the second fork ensures the process can never accidentally reacquire a controlling terminal. Closing standard file descriptors and reopening them to /dev/null prevents the daemon from blocking on I/O that no one will ever read. These steps are standard Unix daemon hygiene and apply to Ruby in exactly the same way they apply to C.

With Block

# Block version - runs block in child
fork do
  puts "Child: #{Process.pid}"
end
puts "Parent: #{Process.pid}"

Handling child exit

After a child process finishes, the kernel holds its exit status in a zombie entry until the parent collects it with one of the Process.wait family of methods. Failing to wait on children causes zombie processes to accumulate, each consuming a process-table slot until the parent exits. Ruby wraps the underlying waitpid(2) system call and lets you choose between a blocking wait, a non-blocking poll, or a loop that collects multiple children as they finish.

# Parent can wait for specific child
pid = fork { sleep 1 }
Process.wait(pid)
puts "Child finished"

# Non-blocking check
pid = fork { }
if Process.waitpid(pid, Process::WNOHANG)
  puts "Child done"
end

The Process::WNOHANG flag makes waitpid non-blocking. If the child has not exited yet, the call returns nil immediately instead of pausing the parent. This is useful when the parent has other work to do and wants to check on children periodically rather than stalling until a specific child finishes. With multiple children, the parent can loop through PIDs and collect exit statuses as they become available.

Exit Status

pid = fork { exit(42) }
_, status = Process.wait2(pid)
puts status.exitstatus  # => 42

Process.wait2 returns a two-element array containing the PID and a Process::Status object. The status object provides more than just the exit code: you can also check whether the process exited normally, was killed by a signal, or dumped core. For most simple cases, exitstatus gives you the integer the child passed to exit, but the full status object is there when you need a more detailed picture of how the child ended.

Important Notes

Resource Management

# Children inherit file descriptors
# Close what you don't need
child = fork do
  STDIN.close
  # ...
end

Each child inherits every open file descriptor from the parent, including sockets, pipes, and file handles that the child may never use. Closing unneeded descriptors early in the child process prevents the kind of subtle bug where a child keeps a pipe or socket open long after the parent expects it to be closed. In server processes that spawn many children, leaking file descriptors can exhaust the system’s open-file limit quickly.

Copy-on-Write

# Modern OS uses copy-on-write
# Parent and child share memory until modified
# Efficient for read-heavy tasks

On platforms without fork

Windows does not provide a POSIX fork system call, so Ruby’s Kernel#fork method simply does not exist on that platform. Attempting to call fork on Windows raises a NotImplementedError. The portable alternative is the spawn family of methods, which create a new operating-system process by executing a command rather than by copying the current process. This means the child starts with a fresh memory space instead of inheriting the parent’s state, which changes the programming model but covers the same use cases for external process management.

# On Windows, fork not available
# Use spawn instead
spawn("ruby", "script.rb")

Windows does not have a native fork system call, so Ruby’s fork is not available on that platform. The spawn method and the Process.spawn family are the portable alternatives: they create a new process by launching an external command rather than duplicating the current process. If you need cross-platform process creation, check RUBY_PLATFORM at startup and provide separate code paths for Unix and Windows environments.

Comparison with Threads

# fork - separate processes, no GIL
# threads - same process, shared memory

Threads share memory, which makes communication cheap but requires careful synchronisation with mutexes or other primitives. Forked processes have separate memory spaces, which eliminates data races but means you need explicit IPC for communication. The trade-off between the two concurrency models depends on whether your workload is CPU-bound (fork benefits from multiple cores without the GIL limitation) or I/O-bound (threads are lighter weight and simpler to manage).

Treat fork as a process boundary

fork is powerful because it creates a new process with its own execution path, not just another thread in the same memory space. That makes it useful for isolation, parallel work, and long-running tasks that should not block the caller. The trade-off is that process management becomes part of the job: you need to think about child lifetime, cleanup, and whether the child should keep running after the parent finishes. That extra responsibility is the price of the extra separation.

Keep the child focused

The child process is easiest to manage when it does one job and exits cleanly. That might mean handling one task, writing one file, or running one worker loop under explicit supervision. If the child inherits more resources than it needs, close the extras early so the process shape stays clear. A small child process is easier to understand and less likely to leave behind strange state in the parent. In other words, keep the boundary sharp so the work stays local.

Wait on purpose

Once you start forking, waiting becomes part of the design. Sometimes the parent should block until a child finishes, and sometimes it should supervise many children and collect them later. Either way, the code should make the choice visible. That clarity matters because fork is not a casual helper; it changes how the program runs. The safest patterns are the ones that show where the child starts, what work it does, and how the parent learns that the work is done.

See Also