Processes, PIDs, signals and threads
coreintermediateA **process** is one running program with its own memory. Its **PID** is the number the kernel gives it, and the number every tool asks you for. A **signal** is a one-byte message you send to a process — `SIGTERM` asks it to stop, `SIGKILL` makes the kernel stop it. A **thread** runs inside a process and shares that process's memory, which is why threads are cheaper and why one thread's crash can take the whole process with it.
Think of it as
A Django deployment is a small tree of processes, and almost every operational question is really a question about that tree. systemd starts one gunicorn *master*; the master forks N *workers*; each worker imports your settings, opens its own database connections and serves requests. The consequences follow directly from "own memory". A module-level cache is per worker, so eight workers hold eight copies and a value written in one is invisible in the others. `CONN_MAX_AGE` keeps a connection per worker, so connection count is a function of process count, not of traffic. A memory leak grows in one worker and is fixed by that worker exiting, which is what `--max-requests` automates. PIDs are how you address one node of that tree. They are assigned by the kernel, reused after a process exits, and they change on every restart — so a PID is fine to type into `kill` right now and useless to store in a config file. The parent PID (PPID) is what lets you read the tree: every worker's PPID is the master's PID, which is how `ps -f` shows you the indentation. Signals are the interface to a running process, and only a few matter. `SIGTERM` (15) is the polite stop: gunicorn treats it as graceful shutdown, waiting for workers to finish in-flight requests up to `graceful_timeout`. `SIGKILL` (9) cannot be caught, blocked or ignored — the process does not run another instruction, so no cleanup happens, no connection is closed politely, and any in-flight request is simply lost. That asymmetry is the reason a deploy sends TERM first and KILL only as a last resort, and the reason your container platform's "termination grace period" must be longer than your graceful timeout, or the platform is the one sending the KILL. `SIGHUP` (1) is conventionally "reload your configuration", and gunicorn implements exactly that. Threads share the process's memory, so they are cheap to create and communicate through ordinary variables — but in CPython the global interpreter lock means threads do not give you parallel CPU work; they give you concurrency while waiting on I/O. That is why gunicorn's `gthread` worker helps a database-bound Django app and does nothing for a CPU-bound one, and why "add threads" and "add workers" are answers to different problems.
What we're doing: Find the gunicorn master on a box you have just been handed, read its worker tree, and reload it without dropping a request.
- 3–4
- `pgrep -a -f` matches against the full command line, which is how you find a process by what it *is* rather than by a PID you would have to have known already.
- 8–13
- The PPID column is the tree. Every worker points at 4812, so signalling 4812 is signalling the group — you almost never signal an individual worker.
- 15–17
- RSS is resident memory per process — `ps` documents it as "the non-swapped physical memory that a task has used". Three workers running the same code with very different RSS is the signature of a per-request leak, and it is why `--max-requests` recycles workers.
- 19–22
- HUP is gunicorn's documented reload: "reload configuration, spawn new workers, and gracefully stop old ones". The listening socket never closes, so no client sees a refused connection.
- 24–30
- New PIDs prove the workers were replaced rather than restarted in place, and the reset RSS proves the leaked memory went with the old process.
Why this works: You located the running server without prior knowledge, read the parent/child structure that explains per-worker memory and connections, and replaced every worker without dropping a connection.
Killing the workers instead of the master
Wrong
Better
What you see: A burst of 502s in the load balancer log at the exact second you ran the command, and application logs that simply stop mid-request with no traceback.
Why: The master exists to own the listening socket and supervise its children, so it is the correct address for every lifecycle instruction. Killing workers directly bypasses the drain: `-9` is `SIGKILL`, which "cannot be caught, blocked, or ignored", so the worker does not finish the response it is writing, does not close its database connection, and does not run any shutdown hook. The master then does exactly its job and forks replacements, which makes the damage look self-healing while every request in flight was dropped. Signal the master and let it manage its own children.
- A tree diagram. At the top, systemd, PID 1, is the parent of the gunicorn master process, PID 4812.
- The gunicorn master has three children drawn below it: worker processes with PIDs 4813, 4814 and 4815. Each worker box notes that it holds its own memory and its own database connection.
- A separate branch on the right shows a Celery master, PID 5001, with one worker child, PID 5002, drawn to show that background work is its own process tree.
- An arrow labelled SIGTERM points at the gunicorn master only, with a note that the master forwards a graceful stop to its workers.
- A red note states that SIGKILL sent to a worker loses that worker in-flight requests, because no cleanup code runs.
The signals worth knowing, and what gunicorn does with each
Together
Process or thread — what you actually get
Together
Remember: A Django deployment is a process tree: systemd → gunicorn master → N workers, each with its own memory and its own database connections — which is why connection count follows process count, not traffic. PIDs are kernel-assigned and reused, so never store one. Signal the master, not the workers: `SIGTERM` drains, `SIGHUP` reloads, and `SIGKILL` (9) "cannot be caught, blocked, or ignored", so it loses every in-flight request. Threads share memory and, under the GIL, buy concurrency while waiting on I/O — not parallel CPU.
See also: looking at a running box · systemd the journal and cron · worker multiplication and connection exhaustion · servers workers and lifecycle

