TechEarl

Bash Job Control: fg, bg, jobs, and nohup

Bash job control reference: suspend with Ctrl-Z, resume in foreground with fg or background with bg, list with jobs, target jobs by %1/%+/%-, and keep a process alive past logout with nohup, disown, or setsid.

Ishan Karunaratne⏱️ 12 min readUpdated
Share thisCopied
Bash job control reference: Ctrl-Z to suspend, fg and bg to resume, jobs to list, kill %1 by job spec, and nohup, disown, and setsid to survive logout.

Job control is how a single Bash session juggles more than one running command. Press Ctrl-Z to suspend the foreground job, type bg to let it keep running in the background, fg to pull it back to the foreground, and jobs to list everything the current shell owns. Each job has a number you target with a % job spec: fg %1, kill %2, bg %+. None of that survives logout though, because a backgrounded job is still a child of the shell and dies with it. To keep a process alive past the terminal closing, start it with nohup, or disown it after the fact, or detach it from the session entirely with setsid. Below is the full reference: suspend and resume, the job spec syntax, listing and signalling, and the three ways to outlive the shell.

How does job control work in Bash?

Job control is the Bash feature that lets one interactive shell start, suspend, resume, and signal multiple commands, each tracked as a job. Start a command with a trailing & to launch it in the background, or press Ctrl-Z to suspend the command currently running in the foreground. bg resumes a suspended job in the background; fg brings a background or suspended job to the foreground. jobs lists all jobs the current shell controls, each with a job number. You reference a job by its number with a percent sign: %1, %2, plus %+ (or %%) for the current job and %- for the previous one. kill %1 sends a signal to job 1. A backgrounded job is still a child of the shell, so it dies on logout unless you protect it with nohup, disown, or setsid. Job control is on by default in interactive shells; non-interactive scripts run with it off. For the loops and long-running commands you will most often background, see Bash While Loops.

Jump to:

Suspend, background, and foreground

The core loop is three keystrokes and two commands.

bash
# Start a long job in the foreground
tar czf backup.tar.gz /var/www

# Press Ctrl-Z, Bash suspends it and hands you the prompt back
^Z
[1]+  Stopped                 tar czf backup.tar.gz /var/www

# Let it keep running in the background
bg
[1]+ tar czf backup.tar.gz /var/www &

# Pull it back to the foreground later
fg
tar czf backup.tar.gz /var/www

What each step does:

  • Ctrl-Z sends SIGTSTP, which stops (pauses) the foreground process and returns you to the prompt. The job is frozen, not running, until you resume it.
  • bg resumes the most-recently-stopped job in the background. The command keeps running but you get your prompt back. It is the equivalent of having started it with & in the first place.
  • fg moves a job to the foreground and connects it to your terminal again, so its output goes to the screen and Ctrl-C reaches it.

You can also background a command from the start with a trailing &:

bash
sleep 300 &
[1] 48213

Bash prints the job number in brackets ([1]) and the process ID (48213). The job number is the shell's local handle; the PID is the system-wide one.

Listing jobs with jobs

jobs shows every job the current shell owns:

bash
jobs
[1]   Running                 sleep 300 &
[2]-  Running                 rsync -a src/ dest/ &
[3]+  Stopped                 vim notes.txt

The markers matter:

  • + is the current job, the default target when you type fg or bg with no argument. Here that is job 3.
  • - is the previous job, the second default. Here that is job 2.
  • Running means it is executing in the background; Stopped means it is suspended (waiting for fg or bg).

Useful flags:

bash
jobs -l    # also show each job's PID
jobs -p    # show PIDs only (handy for scripting: kill $(jobs -p))
jobs -r    # only running jobs
jobs -s    # only stopped jobs

jobs -l is the one I reach for most, because it bridges the job number to the PID when I need to hand the PID to another tool.

Job specs: %1, %+, %-, %string

A job spec is how you name a job to fg, bg, kill, wait, and disown. It always starts with %:

Job specRefers to
%nJob number n (e.g. %1, %2).
%+ or %%The current job (the one marked +).
%-The previous job (the one marked -).
%stringThe job whose command starts with string.
%?stringThe job whose command contains string.
bash
fg %2          # foreground job 2
bg %-          # background the previous job
kill %1         # signal job 1
fg %vim         # foreground the job whose command starts with "vim"
fg %?notes      # foreground the job whose command contains "notes"

A bare number is not a job spec. fg 2 returns fg: 2: no such job because Bash expects the leading %; get in the habit of writing fg %2. The %string forms are convenient interactively but fragile in scripts, where the job number is unambiguous.

Signalling jobs with kill

kill accepts job specs, not just PIDs, when job control is on:

bash
kill %1                 # send SIGTERM (15) to job 1, polite stop
kill -STOP %2           # suspend job 2 (same as Ctrl-Z would)
kill -CONT %2           # resume a stopped job
kill -9 %1              # SIGKILL, last resort, no cleanup

kill -9 (SIGKILL) cannot be caught or ignored, so the process gets no chance to flush buffers or remove temp files. Reach for plain kill %1 (SIGTERM) first and only escalate to -9 if the process ignores it. To send a signal to every job at once, expand the PIDs: kill $(jobs -p).

wait blocks until a job finishes, which is the standard way to parallelise work and then collect it:

bash
process_chunk a &
process_chunk b &
process_chunk c &
wait              # block here until all three background jobs finish
echo "all chunks done"

This pattern, fan out with &, then wait, is the simplest concurrency Bash offers. It works the same inside scripts and inside a Bash for or while loop that launches one background job per iteration.

Surviving logout: nohup, disown, setsid

A backgrounded job is still attached to your login session. When the terminal closes, the kernel sends SIGHUP ("hangup") to the session's processes, and most of them die. Three tools break that link.

nohup runs a command immune to SIGHUP, redirecting its output to nohup.out:

bash
nohup ./long-import.sh &
# nohup: ignoring input and appending output to 'nohup.out'

nohup only blocks the hangup signal; the trailing & is what backgrounds it. Redirect explicitly if you want the output somewhere other than nohup.out:

bash
nohup ./long-import.sh > import.log 2>&1 &

disown is the after-the-fact fix when you already started a job and only then realised you need to log out. It removes the job from the shell's job table so the shell will not send SIGHUP to it on exit:

bash
./long-import.sh &
[1] 50912
disown -h %1      # mark job 1 to not receive SIGHUP, keep it in the table
# or
disown %1          # remove job 1 from the table entirely

disown -h keeps the job listed but shields it from the hangup; plain disown drops it from jobs output altogether. To detach in bulk, disown -a drops every job and disown -r drops only the running ones (leaving stopped jobs alone). Either way the process keeps running after logout. The catch: disown does not redirect output, so if the original terminal had stdout open to the screen, writes can fail once that terminal is gone. Redirect to a file before disowning if the process is chatty.

setsid starts the command in a brand-new session with no controlling terminal, so there is no session to hang up in the first place:

bash
setsid ./long-import.sh > import.log 2>&1 < /dev/null

setsid is the cleanest detach because the process is never a child of your shell's session. It is the closest thing to a one-line daemoniser. Redirect all three streams (stdin from /dev/null, stdout and stderr to a file) since the new session has no terminal to attach them to.

Which detach method to use

MethodWhen you decideOutput handlingBest for
nohup cmd &Before startingAuto-redirects to nohup.outThe default "start and walk away".
disown %1After it is already runningNone (redirect first)"I forgot to nohup it."
setsid cmdBefore startingYou redirect all three streamsA clean detach with no terminal at all.
tmux / screenBefore startingFull interactive session preservedAnything you will want to reattach to and watch.

For a process you genuinely need to revisit, reattach to, and interact with, none of these three beats a terminal multiplexer. nohup, disown, and setsid are fire-and-forget; tmux and screen give you the session back. Reach for the multiplexer when "keep it alive" really means "let me come back to it".

Common pitfalls

1. Expecting backgrounded jobs to survive logout on their own. A trailing & does not protect against SIGHUP. Use nohup, disown, or setsid if you are closing the terminal.

2. Forgetting the % in a job spec. kill 1 targets PID 1 (init/systemd, which the kernel will not let you kill anyway), while a bare number like kill 4823 would happily signal whatever unrelated process holds that PID. kill %1 targets job 1. The % is not optional.

3. disown without redirecting output. Once the controlling terminal closes, a disowned process writing to the old stdout can get SIGPIPE or fail. Redirect to a file before you disown anything chatty.

4. Job control is off in scripts. Job numbers and fg/bg are interactive-shell features. Inside a non-interactive script you still get & and wait, but % job specs and Ctrl-Z semantics behave differently. Use PIDs (pid=$!) in scripts.

5. Ctrl-Z stops, it does not background. Suspended is not the same as running. A job left Stopped is frozen and doing no work until you bg or fg it. Watch for Stopped in jobs output.

6. Trying to fg a job from a different shell. Jobs belong to the shell that started them. Open a new terminal and the job table is empty there; the process is still visible in ps, but not as a job.

7. Logout warning: "There are stopped jobs." Bash refuses to exit while jobs are Stopped, to stop you from accidentally killing paused work. Resume them, kill them, or type exit again to force it.

What to do next

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsBashJob ControlShell ScriptingLinuxnohupdisown

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Schedule a Cron Job on Linux

Schedule a recurring task on Linux with cron: open your crontab, write the five-field schedule, point it at a command, and avoid the environment and day-of-week traps that make jobs run at the wrong time.

How to Check Your Node.js and npm Version

Run node --version and npm --version to see what you have installed. This covers every way to check Node and npm, finding which install is on PATH, reading the version inside a script, and the gotchas with version managers and multiple installs.