Table of Contents

  1. Introduction
  2. What Is Daemon Mode?
    1. Historical Background
    2. Key Characteristics of a Daemon
  3. Why a Session Supervisor Is Needed
    1. The Limitations of Traditional Daemons
    2. User Sessions vs. System Sessions
  4. Popular Session Supervisors
    1. systemd‑user
    2. launchd (macOS)
    3. Upstart & runit
    4. [Supervisord (Python)]#supervisord-python)
  5. Designing a Daemon for Supervision
    1. Daemonizing vs. “No‑Daemon” Approach
    2. Signal Handling & Graceful Shutdown
    3. Logging Strategies
  6. Practical Example: A Simple Go Service Managed by systemd‑user
    1. Service Code
    2. systemd Unit File
    3. Testing the Supervision Loop
  7. Advanced Topics
    1. Socket Activation
    2. Dependency Graphs & Ordering
    3. Resource Limits (cgroups, ulimits)
  8. Troubleshooting Common Pitfalls
  9. Conclusion
  10. Resources

Introduction

Long‑running background processes—daemons—are the invisible workhorses that keep modern operating systems functional. From web servers and database engines to personal notification agents, daemons provide services without direct user interaction. Yet writing a daemon that behaves well under every circumstance is far from trivial. Over the past two decades, the session supervisor model has emerged as a robust solution to many of the classic daemon‑related headaches.

In this article we will:

  • Define daemon mode and explain why the term matters today.
  • Examine the role of a session supervisor and how it differs from a traditional init system.
  • Compare the most widely used supervisors (systemd‑user, launchd, Upstart, runit, Supervisord).
  • Walk through a complete, production‑ready example of a Go service managed by systemd --user.
  • Dive into advanced topics such as socket activation, dependency ordering, and resource control.
  • Provide a troubleshooting checklist for the most common integration problems.

By the end, you should be able to design, implement, and supervise a daemon that starts reliably, recovers gracefully from failures, and integrates cleanly with the host operating system.


What Is Daemon Mode?

Historical Background

The concept of a daemon originates from early UNIX where background processes were forked, detached from the controlling terminal, and placed into a special “daemon” state. The term itself is a nod to the Greek mythological daemon, an unseen spirit that works behind the scenes. Early daemons (e.g., cron, syslogd) followed a set of conventions:

  1. Double‑fork to become a child of init (PID 1).
  2. Close all file descriptors inherited from the parent.
  3. Redirect standard streams to /dev/null or log files.
  4. Run with a restricted user/group for security.
  5. Handle signals (SIGTERM, SIGHUP) for reload/restart.

These steps were manually coded into each program, leading to duplicated boilerplate and a high likelihood of subtle bugs.

Key Characteristics of a Daemon

CharacteristicDescription
DetachmentThe process runs without a controlling terminal.
Long‑livedIt stays alive for the system’s uptime or until explicitly stopped.
Self‑ManagementHandles its own PID file, logging, and signal handling.
AutostartTypically started by init or a similar supervisor at boot.
RecoveryShould restart automatically after crashes, if possible.

When a daemon is well‑behaved, it respects these conventions and coexists peacefully with other services. However, as applications grew more complex (containers, per‑user services, desktop agents), the traditional model showed cracks—especially around session awareness.


Why a Session Supervisor Is Needed

The Limitations of Traditional Daemons

Traditional init systems (sysvinit, early systemd in system mode) launch daemons in the system context (root or a dedicated service account). This works for server‑side components but introduces problems for user‑level services:

  • No access to user‑specific resources (e.g., DBus, Wayland sockets, X11).
  • Lifecycle mismatch – system daemons survive user log‑out, leading to orphaned processes.
  • Permission barriers – a system daemon cannot easily read/write files in a user’s home directory without complex ACLs.

Additionally, many modern tools (e.g., ssh-agent, gpg-agent, notification daemons) need to start once per login session, not once per boot.

User Sessions vs. System Sessions

A session is a collection of processes that share a common login context, environment variables, and access to user‑level IPC mechanisms. In Linux, a session is represented by a session leader (often the login manager like gdm, lightdm, or sshd) and a session ID.

A session supervisor runs inside that session, managing child processes that belong to the same user. It provides the same guarantees that systemd gives for system services—automatic restart, dependency handling, resource limits—while being aware of the user’s environment.

Key benefits:

  • Automatic start on login (or on demand) without root privileges.
  • Graceful termination on logout (no orphaned background processes).
  • Access to per‑user DBus, XDG runtime directories, and secret stores.
  • Uniform tooling – the same systemctl --user commands work for both system and user services.

systemd‑user

systemd has a user mode (systemd --user) that mirrors the system mode but runs under a regular user account. It is started automatically by most modern login managers via a user manager socket ($XDG_RUNTIME_DIR/systemd/user). The command-line interface is the familiar systemctl --user.

  • Pros: Seamless integration with the rest of systemd; supports socket activation, timers, and cgroup v2; works on most modern Linux distros.
  • Cons: Requires a recent systemd version; may be disabled on minimal containers.

launchd (macOS)

macOS uses launchd as both its system and per‑user init system. Per‑user agents are placed in ~/Library/LaunchAgents. launchctl is the command‑line tool.

  • Pros: Native to macOS; supports KeepAlive, Throttling, and resource limits out of the box.
  • Cons: Proprietary format (plist XML) can be verbose; limited to Apple platforms.

Upstart & runit

Both Upstart (Ubuntu’s former init) and runit (a cross‑platform supervisor) provide per‑user supervision via a user session script. They are less common today but still appear in embedded systems.

  • Pros: Simpler than systemd in some respects; runit’s sv tool is tiny and fast.
  • Cons: Lacks deep integration with modern Linux features (cgroups, journald).

Supervisord (Python)

supervisord is a language‑agnostic process control system written in Python. While not a native OS supervisor, it can be launched as a per‑user daemon and manage a set of child processes via a simple INI‑style configuration.

  • Pros: Portable; rich web UI and XML‑RPC API; easy to embed in Python applications.
  • Cons: Doesn’t integrate with systemd’s socket activation; requires a separate Python runtime.

Designing a Daemon for Supervision

Daemonizing vs. “No‑Daemon” Approach

Modern supervisors (systemd, launchd, runit) encourage the “no‑daemon” pattern: the service runs in the foreground, and the supervisor handles detachment, PID tracking, and restarts. This eliminates the need for a double‑fork and reduces complexity.

// Bad: self‑daemonizing
func main() {
    if os.Getppid() != 1 {
        // fork and exit parent...
    }
    // rest of service...
}

// Good: foreground process
func main() {
    // do not fork; let the supervisor manage it
    runServer()
}

When to still daemonize? Rarely—only if you must support legacy init systems that lack supervision capabilities.

Signal Handling & Graceful Shutdown

A well‑supervised daemon must respond to the signals the supervisor sends:

SignalTypical MeaningSupervisor Use
SIGTERMRequest graceful terminationsystemctl stop …
SIGINTInterrupt (often same as SIGTERM)Manual kill
SIGHUPReload configuration without exitsystemctl reload …
SIGUSR1Custom – e.g., rotate logsOptional

Example in Go:

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(),
        syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
    defer cancel()

    go func() {
        // Simulated work loop
        for {
            select {
            case <-ctx.Done():
                log.Println("Shutdown signal received")
                // perform cleanup here
                return
            default:
                // main work
                time.Sleep(2 * time.Second)
                log.Println("tick")
            }
        }
    }()

    <-ctx.Done() // block until signal
    log.Println("Exiting")
}

Logging Strategies

Supervisors provide centralized logging:

  • systemdjournalctl
  • launchd/var/log/system.log or per‑user log files
  • supervisord → its own log files and tail -f

Best practice: write to stdout/stderr and let the supervisor capture the output. Avoid writing directly to files unless you need persistent logs beyond the supervisor’s retention policy.

log.SetOutput(os.Stdout) // ensures logs go to journal

Practical Example: A Simple Go Service Managed by systemd‑user

We’ll build a tiny HTTP server that reports the current time, then create a systemd --user unit that starts it on login, restarts on failure, and captures logs.

Service Code

Create ~/go/src/daemon-time/main.go:

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func timeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Current time: %s\n", time.Now().Format(time.RFC1123))
}

func main() {
    // Log to stdout so systemd captures it
    log.SetOutput(os.Stdout)

    http.HandleFunc("/", timeHandler)

    srv := &http.Server{
        Addr:    ":8080",
        Handler: nil,
    }

    // Graceful shutdown handling
    go func() {
        sigc := make(chan os.Signal, 1)
        signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
        sig := <-sigc
        log.Printf("Received %s, shutting down...", sig)
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        if err := srv.Shutdown(ctx); err != nil {
            log.Printf("Shutdown error: %v", err)
        }
    }()

    log.Println("Starting HTTP server on :8080")
    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatalf("ListenAndServe error: %v", err)
    }
    log.Println("Server stopped")
}

Build the binary:

go build -o $HOME/.local/bin/daemon-time ~/go/src/daemon-time/main.go
chmod +x $HOME/.local/bin/daemon-time

systemd Unit File

Create $HOME/.config/systemd/user/daemon-time.service:

[Unit]
Description=User‑level time‑report HTTP daemon
After=network.target
Wants=network.target

[Service]
ExecStart=%h/.local/bin/daemon-time
Restart=on-failure
RestartSec=5
# Keep stdout/stderr in the journal
StandardOutput=journal
StandardError=journal
# Optional: limit resources
MemoryLimit=100M
CPUQuota=20%

[Install]
WantedBy=default.target

Explanation of important directives:

  • After=network.target – ensures the network stack is up before the service starts.
  • Restart=on-failure – systemd will automatically restart after a crash.
  • MemoryLimit / CPUQuota – cgroup v2 limits that protect the user’s session from runaway processes.
  • WantedBy=default.target – ties the service to the user’s default target, which is pulled in when the user logs in.

Enabling & Testing

# Reload user manager configuration
systemctl --user daemon-reload

# Enable the service to start on every login
systemctl --user enable daemon-time.service

# Start it immediately for testing
systemctl --user start daemon-time.service

# Check status
systemctl --user status daemon-time.service

# View logs
journalctl --user -u daemon-time.service -f

Open a browser (or curl) to http://localhost:8080/ – you should see the current time.

Simulating a Crash

# Find the PID
pid=$(systemctl --user show -p MainPID daemon-time.service | cut -d= -f2)
# Send SIGSEGV (illegal instruction) to crash
kill -SIGSEGV $pid

Systemd will log the crash and automatically restart the service after the RestartSec interval.


Advanced Topics

Socket Activation

Socket activation lets the supervisor open the listening socket before the service starts, passing the file descriptor to the service. The daemon only runs when there is inbound traffic, reducing idle memory usage.

systemd socket unit (daemon-time.socket):

[Unit]
Description=Socket for daemon-time service

[Socket]
ListenStream=8080
Accept=no

[Install]
WantedBy=sockets.target

Modify daemon-time.service:

[Service]
ExecStart=%h/.local/bin/daemon-time
StandardInput=socket

Now, the service will receive the socket as STDIN, and Go’s net/http can be started with Serve instead of ListenAndServe. This pattern is used by high‑performance services like nginx and systemd-resolved.

Dependency Graphs & Ordering

Complex user environments may have multiple interdependent services (e.g., a DBus service, a notification daemon, a background sync client). Use After= and Requires= to enforce start order and failure propagation.

[Unit]
Requires=dbus.service
After=dbus.service

systemd builds a dependency DAG; cycles are prohibited, and the DAG is evaluated at runtime to start services in parallel when possible.

Resource Limits (cgroups, ulimits)

User managers run under cgroup v2 hierarchy (/sys/fs/cgroup/user.slice/user-1000.slice). You can fine‑tune limits per service:

[Service]
MemoryMax=200M          # hard limit
CPUQuota=30%            # max 30% of a single CPU
IOWeight=500            # relative I/O priority

These limits protect the desktop environment from a single rogue daemon consuming all resources.


Troubleshooting Common Pitfalls

SymptomLikely CauseFix
Service never starts after loginsystemd --user not running (e.g., missing pam_systemd entry)Ensure /etc/pam.d/login includes session required pam_systemd.so and systemd --user is enabled
Logs are emptyStandardOutput=journal missing or journalctl not reading user logsAdd ForwardToSyslog=yes or view with journalctl --user
Service crashes on first requestBinary expects to open its own socket but socket activation is configuredEither disable socket activation or modify code to accept pre‑opened socket (use net.FileListener)
Restart loop (service constantly failing)Unhandled error causing immediate exit (e.g., missing config file)Add RestartPreventExitStatus= or fix the underlying error
No network on startAfter=network.target not sufficient on systems using NetworkManager’s systemd-networkdUse After=network-online.target and Wants=network-online.target

Debugging tip: Use systemctl --user show -p ExecMainStatus,ExecMainPID <service> to see exit codes, and journalctl -xe for recent error messages.


Conclusion

Daemon mode and session supervision have converged into a clean, predictable model for background processes. By letting the supervisor handle detachment, restarts, logging, and resource limits, developers can focus on the core business logic of their services. Whether you are building a lightweight per‑user notification agent, a cross‑platform CLI tool with a background sync component, or a full‑blown server daemon, the principles outlined here apply:

  • Prefer foreground processes; let the supervisor daemonize for you.
  • Implement proper signal handling to cooperate with stop/reload requests.
  • Leverage built‑in logging (stdout/stderr) instead of custom file writes.
  • Use socket activation when possible to reduce idle resource usage.
  • Apply cgroup limits to protect the user session from misbehaving daemons.

With systemd --user as the de‑facto standard on modern Linux distributions, you have a powerful, battle‑tested toolkit at your disposal. The same concepts translate to macOS’s launchd, the portable supervisord library, or minimal supervisors like runit. By mastering these patterns, you’ll write daemons that are robust, maintainable, and friendly to the ecosystems they live in.


Resources