The strace -f flag tells the system that when a process you’re tracing creates a new child process (via fork or clone), you want to follow that child and trace its system calls too.

Let’s see it in action. Imagine a simple Python script that forks:

import os

print(f"Parent PID: {os.getpid()}")
pid = os.fork()

if pid == 0:
    print(f"Child PID: {os.getpid()} from parent {os.getppid()}")
else:
    print(f"Parent {os.getpid()} forked child {pid}")

If you run strace python your_script.py (without -f), you’ll only see the system calls made by the parent process. You’ll see the fork() call, but you won’t see anything that the child does after the fork.

$ strace python your_script.py
execve("/usr/bin/python", ["python", "your_script.py"], 0x7ffd34865064 /* 70 vars */) = 0
...
brk(NULL)                               = 0x559265452000
...
clone(child_stack=0x7ffff7fc7ff0, flags=CLONE_CHILD_SETTID|CLONE_CHILD_CLEARTIDstrace: Process 12345 attached
, parent_tidptr=0x7ffff7fc89d0, tls=0x7ffff7fc89d0) = 12346
...
write(1, "Parent PID: 12345\n", 18Parent PID: 12345
)     = 18
write(1, "Parent 12345 forked child 12346\n", 32Parent 12345 forked child 12346
) = 32
...
exit_group(0)                           = ?
+++ exited with 0 +++

Notice how the child’s PID (12346) is printed, but we don’t see any system calls associated with it after the clone (which is what fork uses internally).

Now, let’s run it with -f:

$ strace -f python your_script.py

The output will be interleaved, showing system calls from both the parent and the child. You’ll see the parent’s actions, then the child’s actions, then potentially more parent actions, and so on.

...
[pid 12345] execve("/usr/bin/python", ["python", "your_script.py"], 0x7ffd34865064 /* 70 vars */) = 0
...
[pid 12345] brk(NULL)                               = 0x559265452000
...
[pid 12345] clone(child_stack=0x7ffff7fc7ff0, flags=CLONE_CHILD_SETTID|CLONE_CHILD_CLEARTIDstrace: Process 12346 attached
, parent_tidptr=0x7ffff7fc89d0, tls=0x7ffff7fc89d0) = 12346
[pid 12346] --- new process ---
[pid 12346] set_tid_address(0x7ffff7fc89d0)         = 12346
[pid 12346] set_robust_list(0x7ffff7fc89e0, 24)     = 0
[pid 12346] getppid()                               = 12345
[pid 12346] write(1, "Child PID: 12346 from parent 12345\n", 36Child PID: 12346 from parent 12345
) = 36
[pid 12345] write(1, "Parent PID: 12345\n", 18Parent PID: 12345
)     = 18
[pid 12345] write(1, "Parent 12345 forked child 12346\n", 32Parent 12345 forked child 12346
) = 32
...
[pid 12346] exit_group(0)                           = ?
[pid 12346] +++ exited with 0 +++
[pid 12345] --- process 12346, finished ---
[pid 12345] munmap(0x7ffff7ddc000, 8192)            = 0
...
[pid 12345] exit_group(0)                           = ?
[pid 12345] +++ exited with 0 +++

Notice the [pid XXXX] prefix on each line. This clearly indicates which process made which system call. The --- new process --- and --- process XXXX, finished --- lines are also strace’s way of signaling the start and end of traced child processes.

The core problem strace -f solves is the "observational blindness" that occurs when a process spawns another. Without -f, strace attaches to the initial process and sees the fork() or clone() system call. This call returns the PID of the new child process to the parent. However, the original strace instance is still attached only to the parent. The child process, now running independently, is not being observed by strace at all. strace -f modifies the ptrace mechanism (the underlying system call strace uses for tracing) to automatically attach to any new children created by the traced process. It does this by instructing the kernel’s ptrace subsystem to notify strace when a PTRACE_TRACEME event occurs, which is how a child signals to its tracer that it wants to be traced.

The primary benefit is debugging complex multi-process applications, like web servers, build systems, or anything that uses a process pool. You can see how data flows between processes, which process is waiting on another, or if a child process is crashing unexpectedly. For instance, if a daemon forks worker processes and you’re only tracing the master, you might miss an error occurring in a worker that causes it to exit prematurely, leaving the master unaware or unable to diagnose the issue.

A lesser-known but powerful aspect is how -f interacts with execve. When a child process calls execve to replace its current program image with a new one, strace -f will continue tracing that same PID but will start showing the system calls of the new program. This is crucial because execve fundamentally changes the process’s identity from the kernel’s perspective in terms of its loaded code, but it doesn’t create a new PID. So, while the PID remains the same, the behavior observed by strace is entirely different.

The next logical step after mastering strace -f is understanding how to filter its output, especially with multiple processes. You might use -e trace=open,read,write to narrow down the calls, and then combine that with -p <pid> if you only want to focus on a specific child process after it’s been forked.

Want structured learning?

Take the full Strace course →