Reliability and resources

Model startup order and readiness

Express real dependencies and application readiness instead of adding arbitrary sleep commands.

Ordering a service after the network target does not prove that a remote database is ready. network-online.target means the local network stack came up. Your database on another machine can still be rebooting, slow, or down.

That gap is where sleep hacks come from:

[Service]
ExecStartPre=/bin/sleep 15
ExecStart=/usr/bin/node /opt/demo-api/server.js

This works until the database takes sixteen seconds. Then it fails, someone bumps the sleep to thirty, and every deploy gets slower. The sleep encodes a guess, not a dependency. I have never seen one of these get smaller over time.

Model what systemd can see

systemd can order units on the same machine. Use that for local dependencies:

[Unit]
Wants=network-online.target
After=network-online.target postgresql.service

If PostgreSQL runs on this host, After=postgresql.service is a real, checkable ordering. Use Requires= instead of Wants= only when your service is useless without the dependency and should fail with it.

For anything remote, the dependency is invisible to systemd. There is no unit for a database on another server. So the application has to handle it.

Let the app retry temporary failures with a bounded backoff. Try the connection, wait a second, try again, wait two seconds, and give up with a clear error after a budget. In Node.js that is a small loop:

for (let attempt = 1; attempt <= 5; attempt++) {
  try {
    await db.connect()
    break
  } catch (err) {
    if (attempt === 5) throw err
    await new Promise(r => setTimeout(r, attempt * 1000))
  }
}

If the app gives up, it exits non-zero. The restart policy from the previous lesson brings it back with a delay. Together, the two recover from a slow dependency without a single sleep.

Readiness is a separate claim

“Started” and “ready to serve” are different events. A Node.js process can be running for two seconds before it has finished connecting to the database and opened its port.

Type=notify closes that gap. The application calls sd_notify() with READY=1 only when it can handle traffic. Units ordered After= it wait for that signal. Use it only when the application can report readiness. Check its documentation before switching the type, because a notify service that never sends the message hangs until the start timeout.

You can see how long each service took to reach ready:

systemd-analyze blame | head -5
15.212s demo-api.service
 2.104s postgresql.service
 1.318s nginx.service
   612ms ssh.service
   201ms cron.service

A fifteen second entry for a small API is a sleep hack, and it stands out immediately in that list.

Try this on a design you know. Take one sleep-based startup workaround and replace it with either a real local dependency (After= a unit on the same host) or an application-level retry with backoff. Then write one sentence stating the boundary: which part systemd guarantees, and which part the application handles.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →