Skip to content
FreeBSDDeep Dive July 11, 2026 4 min readViews unavailable

FreeBSD's rc.d Init System: Scripts, Dependencies, and Service Ordering

How rc.d resolves which service starts before which without a central dependency graph author, and how to write a script that actually plugs into that ordering correctly.

FreeBSD’s rc.d doesn’t have a single, hand-maintained boot-order file listing “start networking, then start sshd, then start cron.” Instead, every service script declares its own dependencies, and rc.d computes a valid ordering from those declarations at boot time — a decentralized dependency system where no single script needs to know about any other script by name, only by the abstract “provision” it needs.

PROVIDE, REQUIRE, and BEFORE: the actual vocabulary

Every rc.d script starts with a block of shell comments that rcorder(8) parses — not executes, parses as plain text — to build the dependency graph before anything actually runs:

#!/bin/sh
# PROVIDE: myservice
# REQUIRE: DAEMON NETWORKING
# BEFORE: LOGIN
# KEYWORD: shutdown

PROVIDE names what this script offers — other scripts reference this name, not the script’s filename, when declaring a dependency on it. REQUIRE lists provisions that must be satisfied (their scripts already run) before this one starts. BEFORE is the inverse: it tells rcorder this script must run before some other named provision, useful when the other script can’t be modified to add a REQUIRE on this one (a common situation with scripts installed by third-party packages).

The key design decision is that these are provisions, not filenames — NETWORKING isn’t a script, it’s an abstract capability that FreeBSD’s own network script (among others) provides, and any script that needs a working network interface declares REQUIRE: NETWORKING without needing to know or care which specific script actually satisfies that requirement on a given system. This indirection is what lets third-party ports add their own rc.d scripts that correctly interleave with the base system’s boot sequence without ever needing to know the base system’s internals or exact script names.

How rcorder actually resolves this into a sequence

At boot, /etc/rc invokes rcorder(8) against the full set of enabled scripts (base system scripts in /etc/rc.d, port-installed ones in /usr/local/etc/rc.d). rcorder builds a directed graph from every script’s declared PROVIDE/REQUIRE/BEFORE lines, then performs what’s effectively a topological sort — repeatedly picking scripts whose requirements are already satisfied, adding them to the output sequence, and marking their provisions as satisfied for the next round — until every script has been placed or an unresolvable cycle is detected.

A cycle (script A requires something only satisfied after script B, which requires something only satisfied after script A) is a genuine configuration error, and rcorder will report it rather than silently picking an arbitrary order — this is a deliberate fail-loud choice, since silently breaking the requested ordering could mean a service starts before a dependency it actually needs is ready, with failure modes ranging from immediate crash to subtle, intermittent misbehavior depending on timing.

Writing a script that fits into this correctly

A minimal, well-formed rc.d script needs the standard header, sources /etc/rc.subr for the shared helper functions, and defines the handful of variables that rc.subr’s generic logic uses to build start/stop/status behavior without the script author reimplementing process management from scratch:

#!/bin/sh
# PROVIDE: myservice
# REQUIRE: DAEMON NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="myservice"
rcvar="myservice_enable"
command="/usr/local/bin/myservice"
command_args="-c /usr/local/etc/myservice.conf"
pidfile="/var/run/${name}.pid"

load_rc_config "$name"
run_rc_command "$1"

load_rc_config pulls in the myservice_enable="YES" style variables from /etc/rc.conf (and /etc/rc.conf.d/myservice if present), and run_rc_command "$1" dispatches to the correct generic start/stop/restart/status logic based on how the script was invoked, using the command/pidfile/etc. variables to know what process it’s actually managing. This is why most rc.d scripts in the base system are short — nearly all the actual process-supervision logic (checking if it’s already running, sending the right signal to stop it, reporting status) lives once in rc.subr, not duplicated in every script.

The KEYWORD line and shutdown ordering

The KEYWORD: shutdown line matters specifically because rc.d’s dependency graph is used for both boot-time startup and shutdown — but not in the same direction by default. A script marked with KEYWORD: shutdown is included in the shutdown sequence at all (scripts without it are assumed not to need explicit stopping, which is common for scripts that just perform a one-time setup action rather than running a persistent daemon). For services that do run a persistent daemon, omitting KEYWORD: shutdown is a common, subtle mistake — the service will start correctly at boot but never receive a clean stop signal during shutdown, potentially leaving state inconsistent depending on what the daemon itself does (or doesn’t do) on an unclean termination via the eventual SIGKILL that happens regardless at the end of shutdown.

Debugging ordering problems

rcorder -s nostart /etc/rc.d/* /usr/local/etc/rc.d/* prints the computed order without actually running anything — the single most useful diagnostic when a service appears to start too early or too late relative to something it depends on. It’s also the fastest way to confirm that a third-party port’s rc.d script has correctly declared its dependencies, rather than guessing from boot-log timestamps after the fact, which conflates “when did this script run” with “why did rcorder decide to place it there,” and only the latter question actually explains ordering problems when they occur.