Embedded and Daemon Mode
This guide covers configuring mod_wsgi’s two process models, embedded mode and daemon mode. Each is configured through its own directive set:
Embedded mode is configured through Apache’s MPM directives (
MaxRequestWorkers,StartServers, the spare-worker / spare-thread directives).Daemon mode is configured through mod_wsgi’s
WSGIDaemonProcess,WSGIProcessGroupandWSGIApplicationGroupdirectives. See WSGIDaemonProcess for the per-option reference.
The Processes And Threading guide covers the underlying Apache MPM and Python sub interpreter model that this page builds on. Read that page first if the term “MPM” or “sub interpreter” is unfamiliar.
The two process models
In embedded mode the application runs inside Apache’s
child worker processes. The Python interpreter is loaded
into Apache’s address space and the WSGI application
shares the process with every other Apache module:
mod_php, static-file serving, mod_ssl session
handling, and so on. Process and thread counts are
dictated by Apache’s MPM tuning.
In daemon mode mod_wsgi creates a dedicated set of processes running just the WSGI application. The Apache child processes act as proxies: they receive the request, hand it to a daemon process over a UNIX domain socket, and relay the response back. The daemon processes are managed by the Apache parent (started, restarted, recycled), and Apache’s own request-handling children need not load the Python interpreter to dispatch a request to the daemon. (Auth scripts and dispatch scripts, if configured, are an exception: those run in an embedded Python interpreter inside the Apache child even when the application itself is in a daemon process. See WSGIAuthUserScript and WSGIDispatchScript.)
For most deployments daemon mode is the right default. Compared to embedded mode it gives:
A different user account from Apache, so the application runs with the privileges it actually needs rather than the broader set Apache itself runs with.
An address space the application owns, so a memory leak or a crash takes down only the daemon pool.
Process and thread counts decoupled from Apache’s MPM tuning, so the application can be sized for its own workload.
Recycling triggers (request count, wall-clock interval, CPU time, idle time, memory, deadlock) that operate per-pool without restarting Apache.
A reload mechanism (touch the WSGI script) that does not require an Apache restart.
The trade-off is one extra process hop per request and the operational surface of the daemon pool itself. For a Python web application both costs are small relative to the benefits.
Daemon mode is not available on Windows; mod_wsgi on Windows supports only embedded mode. Embedded mode itself is supported on all platforms.
To enforce daemon-only deployment and reject any configuration that would otherwise fall back to embedded mode:
WSGIRestrictEmbedded On
This is recommended for any production deployment that should be in daemon mode. See WSGIRestrictEmbedded.
The rest of this page splits into two parts: the first covers configuring embedded mode, and the second (considerably larger) covers configuring daemon mode.
Embedded mode
What embedded mode looks like
A minimal embedded-mode configuration is a script alias with no daemon-process delegation:
WSGIScriptAlias / /srv/myapp/myapp.wsgi
<Directory /srv/myapp>
Require all granted
</Directory>
The application runs in whichever Apache child accepts the request, with concurrency inherited from the active MPM:
Under
preforkeach child handles one request at a time. Concurrency is achieved by spawning more children.Under
workerandeventeach child runs a thread pool. Concurrency is the product of children and threads.Under Windows
mpm_winntthere is one child running a thread pool.
When to use embedded mode
Embedded mode is the right choice in a small number of cases:
Windows. Daemon mode is not implemented on Windows; embedded mode is the only option there.
Single-application Apache instances where the isolation, recycling and per-tenant features of daemon mode are not needed and the simpler configuration is preferred.
Tooling that requires
wsgi.multiprocessto be False. Some interactive debuggers require this, which an embedded-mode deployment with a single prefork child trivially satisfies.
For most production deployments daemon mode is the recommended default; the “Daemon mode” section below covers its features and configuration.
If embedded mode has not been chosen deliberately, set
WSGIRestrictEmbedded On (above) so Apache rejects any
configuration that would fall back to embedded mode.
Apache MPM tuning
Embedded-mode concurrency is set by Apache’s MPM directives. The relevant ones:
MaxRequestWorkersis the upper bound on simultaneous requests Apache will handle. (WasMaxClientsin Apache 2.2.)StartServersis the number of child processes spawned at startup.MinSpareServers/MaxSpareServers(prefork) orMinSpareThreads/MaxSpareThreads(worker, event) bound the number of idle workers Apache maintains, which drives spawn-and-reap dynamics under fluctuating load.ServerLimitand (for worker, event)ThreadLimitare hard ceilings beyond which the configuration cannot grow at runtime.
Each Apache child or thread runs a copy of the WSGI application, so these directives directly determine how many concurrent requests the application handles. See Processes And Threading for the per-MPM walkthrough that this configuration sits on top of.
Under mod_wsgi-express --embedded-mode, the four
--max-clients, --initial-workers,
--minimum-spare-workers and --maximum-spare-workers
options are ignored. Express forces a fixed-pool MPM sized
to --processes × --threads (with StartServers,
MinSpare* and MaxSpare* all set to that value),
so concurrency is exactly the product of those two options
regardless of load. To enable Apache’s dynamic
spare-worker management under embedded mode, configure
mod_wsgi inside a manually managed Apache rather than via
express.
Embedded-mode caveats
Embedded mode brings the Python interpreter into Apache’s child workers, with several consequences worth understanding before choosing it:
Shared address space with Apache. A memory leak or crash in the application takes the Apache child with it. Apache will spawn a replacement, but in-flight requests in that child are lost.
Shared with other Apache modules. Other in-process Apache modules (
mod_php,mod_sslsession caches, and so on) sit alongside Python in the same address space. Memory accounting and resource limits apply to the whole.Apache user. The application runs as the user Apache is configured to run as. Embedded mode has no equivalent of daemon mode’s
user=option for running as a different account.No process recycling triggers. The recycling options on
WSGIDaemonProcess(request count, wall-clock interval, CPU time, idle, memory, deadlock) are daemon-mode features. Apache’sMaxConnectionsPerChildis the closest analogue, recycling Apache children after a configured number of connections.No daemon-mode timeouts.
request-timeout,deadlock-timeout,startup-timeout,shutdown-timeout,graceful-timeoutand the others are daemon-mode features.Reload only covers the WSGI script. When
WSGIScriptReloadingis on (the default) and the WSGI script’s modification time changes, the reload happens by dropping the script’ssys.modulesentry and re-importing it in the same Apache child. Other Python modules the application has loaded are not reloaded. To pick up changes elsewhere in the application code base, restart Apache.
Daemon mode
The three configuration directives
Three directives configure daemon mode. They are independent and each does one thing.
WSGIDaemonProcessDeclares a named pool of one or more daemon processes. Sets the user the pool runs as, the number of processes, the threads per process, the Python environment, the recycling triggers, the timeouts, and so on. See WSGIDaemonProcess.
WSGIProcessGroupSelects which named pool a request is dispatched to. The argument is the name of a pool declared with
WSGIDaemonProcess, or one of the special expansions%{GLOBAL}(embedded mode) or%{ENV:variable}(look up the pool name in an environment variable). See WSGIProcessGroup.WSGIApplicationGroupSelects which Python sub interpreter inside the chosen pool runs the application. The argument is a name of your choosing or one of the special expansions
%{GLOBAL}(the main interpreter),%{SERVER}(per-virtual-host),%{RESOURCE}(per-script, the default), or%{ENV:variable}. See WSGIApplicationGroup.
A complete minimal configuration wiring all three together:
WSGIDaemonProcess myapp processes=2 threads=15 \
user=appsvc group=appsvc \
display-name=%{GROUP}
WSGIProcessGroup myapp
WSGIApplicationGroup %{GLOBAL}
WSGIScriptAlias / /srv/myapp/myapp.wsgi
The process-group and application-group options on
WSGIScriptAlias are equivalent to the WSGIProcessGroup
and WSGIApplicationGroup directives and can replace the
two directive lines above for the script-alias-only case:
WSGIDaemonProcess myapp processes=2 threads=15 \
user=appsvc group=appsvc \
display-name=%{GROUP}
WSGIScriptAlias / /srv/myapp/myapp.wsgi \
process-group=myapp application-group=%{GLOBAL}
The script-alias form has one additional behaviour: it triggers auto-preload of the WSGI script in the named daemon process group at startup, so the first request to the application does not pay the import cost. The directive form does not preload.
Sizing the pool
The two knobs are processes= and threads= on
WSGIDaemonProcess. The defaults (one process, fifteen
threads) are conservative and rarely the right answer for
production traffic. Choosing values for both is the single most
consequential tuning decision for daemon mode.
Processes
More processes give:
Independent failure domains. A crash, a wedge on the GIL, or a memory blowout takes out one process; the others keep serving.
Independent CPython GILs. Each process has its own interpreter and its own GIL, so concurrent CPU-bound work runs in parallel across processes. Threads inside a single process do not.
Independent Python heaps. Memory growth in one process does not affect the others, and recycling one process reclaims its memory without disturbing the rest.
The cost of more processes is memory: each process has its own copy of the Python interpreter, the loaded modules, and any in-memory caches the application builds up. Each daemon process initialises Python independently after forking from the Apache parent (Python is not loaded in the parent), so there is no shared interpreter state across processes to amortise either the startup cost or the resident memory footprint against; both are paid in full per process.
Threads
More threads per process give:
Concurrency on I/O-bound work. While one thread is blocked waiting on a database, an HTTP backend, or a file read, other threads in the same process can run.
Lower memory cost than the equivalent extra processes. Threads share the interpreter, the modules, and the heap.
The cost of more threads is GIL contention. Two threads inside one process cannot both be running Python bytecode at the same time. For CPU-bound work the second, third, and Nth threads add contention without adding throughput.
Rules of thumb
Most Python web applications are I/O-bound: each request spends
most of its time waiting on a database, a cache, an HTTP service,
or a template render that itself reads from disk. For that
profile, processes=N threads=15 (where N is roughly the
number of CPU cores you are willing to dedicate to the
application) is a reasonable starting point.
For CPU-bound applications (heavy template work, data
serialisation, image processing inline in the request) drop
threads down. processes=N threads=3 or even
processes=N threads=1 is often a better fit. Adding threads
beyond what the GIL can interleave just adds context-switching
cost.
For applications with very large per-process state (a model
loaded into memory, a large in-process cache) prefer fewer
processes with more threads. The state is duplicated per
process, so processes=8 threads=2 has eight times the memory
cost of processes=1 threads=16 for the same total
concurrency.
For interactive debuggers and any other component that requires
wsgi.multiprocess to be False, omit the processes=
option entirely (see
WSGIDaemonProcess on the
distinction between omitting processes= and setting
processes=1).
Capacity planning
The total request concurrency the pool can handle simultaneously
is processes * threads. Beyond that, requests queue on the
pool’s UNIX socket up to listen-backlog, then sit waiting up
to queue-timeout (if set) before getting a 504. The Apache
child workers calling into the pool are themselves a separate
queue ahead of that one.
Headroom matters. A pool sized to exactly steady-state load has no room to absorb a burst. Aim for steady-state load to leave the pool ~50% busy, so a doubling of incoming traffic still fits without queueing. If the application has a long-tail latency distribution (a few requests that take much longer than the median) bump that headroom further: a wedged thread is a thread not available for the next request.
The natural-log scaling that request-timeout applies (see
“Timeouts” below) is also informed by threads: more threads
implies the pool can tolerate one wedge for longer before
forcing a recycle. This is a property of the timeout machinery,
not of sizing per se, but it is one more reason to think of
threads as a deliberate choice rather than an
unconfigured-default.
Process group patterns
The shape of a daemon process group depends on what you are trying to isolate.
Single global pool
The simplest case. One WSGIDaemonProcess declared at server
scope, all virtual hosts delegate to it:
WSGIDaemonProcess shared processes=4 threads=15 \
user=appsvc group=appsvc \
display-name=%{GROUP}
<VirtualHost *:80>
ServerName www.site1.example
WSGIProcessGroup shared
WSGIScriptAlias / /srv/site1/site1.wsgi
</VirtualHost>
<VirtualHost *:80>
ServerName www.site2.example
WSGIProcessGroup shared
WSGIScriptAlias / /srv/site2/site2.wsgi
</VirtualHost>
Both sites run in the same processes. They get separate Python
sub interpreters by default (the %{RESOURCE} application
group expansion gives each script its own), but they compete
for the same threads and live in the same address space. Pick
this pattern when the sites trust each other (same operator,
same security boundary) and you do not need to size them
independently.
Per-virtual-host pool
One pool per virtual host. Each pool is sized for that site and runs as that site’s owning user:
<VirtualHost *:80>
ServerName www.site1.example
WSGIDaemonProcess www.site1.example \
processes=2 threads=15 \
user=site1 group=site1 \
display-name=%{GROUP}
WSGIProcessGroup www.site1.example
WSGIScriptAlias / /srv/site1/site1.wsgi
</VirtualHost>
<VirtualHost *:80>
ServerName www.site2.example
WSGIDaemonProcess www.site2.example \
processes=2 threads=15 \
user=site2 group=site2 \
display-name=%{GROUP}
WSGIProcessGroup www.site2.example
WSGIScriptAlias / /srv/site2/site2.wsgi
</VirtualHost>
The two sites are now isolated at the process and the OS-user level. A bug in site1 cannot affect site2’s processes; site1 cannot read site2’s files unless the filesystem permissions explicitly allow it. The cost is duplicate Python interpreters and the operational overhead of more pools.
Pool names must be unique across the whole server. www.site1.example
declared inside one virtual host cannot also be declared inside
another, even if the other settings differ.
A virtual host pair on ports 80 and 443 sharing the same server name should declare the pool once (in the first virtual host) and reference it from the second:
<VirtualHost *:80>
ServerName www.site1.example
WSGIDaemonProcess www.site1.example \
processes=2 threads=15 \
user=site1 group=site1
WSGIProcessGroup www.site1.example
...
</VirtualHost>
<VirtualHost *:443>
ServerName www.site1.example
WSGIProcessGroup www.site1.example
...
</VirtualHost>
This avoids running two whole instances of the same application for the two ports.
Per-tenant pool
When multiple unrelated applications share an Apache instance,
each one running as a different OS user, declare one pool per
tenant. The configuration shape is the same as per-virtual-host
above; the difference is that the user accounts are not
operator-trusted relative to each other. See
Security Hardening for the broader hardening picture
including WSGIRestrictProcess, socket ownership, and
filesystem permissions for multi-tenant deployments.
Parallel pools for cutover
A blue/green pattern declares two pools that alternate live and
idle roles across upgrades. The Upgrading An Application
guide covers the full pattern, including the routing layer
(WSGIDispatchScript or mod_rewrite with a map file) and
why the pool names must be stable across upgrades.
Restricting which pools a delegation can choose
When WSGIProcessGroup %{ENV:variable} is used, the pool name
comes from a request-time environment variable, which makes it
possible for a .htaccess file or a RewriteRule to choose
the pool. To prevent that mechanism from selecting an unintended
pool (for example, a pool belonging to a different tenant) use
WSGIRestrictProcess to list the pools that are valid choices
in the relevant context. See
WSGIRestrictProcess.
Application group choice
The application group selects which Python sub interpreter inside the daemon process runs the application. There is one sub interpreter per application group name per process; sub interpreters do not share Python module state.
The default is %{RESOURCE}, which gives each WSGI script its
own application group keyed on host, port, and SCRIPT_NAME.
For most single-application deployments this is what you want:
the script gets its own interpreter, isolated from any other
script in the same pool.
The cases that call for an explicit choice:
%{GLOBAL}Use the main Python interpreter rather than a sub interpreter. This is required for applications that depend on C extensions which do not work correctly outside the main interpreter. NumPy, SciPy, and packages built on them are the most common examples; the failure mode is typically a process crash on import or first use, not a recoverable error.
WSGIApplicationGroup %{GLOBAL}is the safe default if you are unsure whether your dependency tree is sub-interpreter-clean. See “Multiple Python Sub Interpreters” in Application Issues.%{SERVER}All scripts under the same virtual host share one application group. Useful when several scripts in a single virtual host are designed to share Python module state.
- Named application group
Choose your own name. Two scripts that name the same application group share an interpreter; two scripts with different names do not. Used most often when several scripts deliberately share a framework setup.
%{ENV:variable}Pick the application group at request time from an environment variable set by
SetEnvorRewriteRule. Useful for per-user grouping (for example,mod_userdirdeployments where each user’s scripts share one interpreter but are isolated from other users’).
In a multi-process pool, “shares an application group” means
“shares a sub interpreter within the same process”. Two
processes in the same pool each have their own copy of the named
sub interpreter; in-process global data is not shared across
processes regardless of application group name. If you need a
single sub interpreter handling all requests for a tenant,
configure the pool with processes=1 (omitted, not
processes=1: see “Sizing” above).
Per-interpreter GIL and free-threaded Python
Recent Python releases introduced two mechanisms that change the sub interpreter picture: per-interpreter GIL state under PEP 684 (Python 3.12) and optional free-threading under PEP 703 (Python 3.13). mod_wsgi provides WSGIPerInterpreterGIL and WSGIFreeThreading to opt into these on a per-pool basis. Both change the GIL-contention calculus described above. See those directive pages and Processes And Threading for the current support state.
Process recycling
Daemon processes can be configured to recycle automatically based on a number of triggers. The triggers compose: any one that fires causes the process to be replaced.
maximum-requests=nnnRecycle after the process has handled this many requests. A safety net for slow memory leaks. Set high enough that recycling is not the dominant operation (a few thousand to tens of thousands of requests per cycle is typical).
restart-interval=sssRecycle after this many seconds of wall-clock time. A time-based counterpart to
maximum-requestsfor catching slow growth that scales with elapsed time rather than request count, or for periodically flushing in-process state that drifts over time.cpu-time-limit=sssRecycle after the process has accumulated this much CPU time. Catches CPU runaway. The limit is cumulative from process start, so a process will eventually hit it in normal use; size accordingly.
inactivity-timeout=sssRecycle after this many seconds with no requests in flight and no new requests arriving. Reclaims memory from infrequently used applications. Note the first request after a recycle pays the application’s import cost again.
memory-limitandvirtual-memory-limitHard limits enforced via
setrlimit(). The process is killed when it exceeds the limit. Not implemented on all platforms (notably macOS); test before relying on it.deadlock-timeout=sssRecycle when a potential GIL deadlock is detected. This catches the case where a Python C extension fails to release the GIL inside a long blocking operation, which would otherwise wedge the whole process indefinitely.
When maximum-requests, restart-interval, or
cpu-time-limit fires, and graceful-timeout is set, the
process continues serving in-flight requests and accepting new
ones for up to that many seconds, restarting as soon as it
reaches an idle state. This avoids the bursty behaviour that
unconditional restart can cause when several processes hit the
trigger at once. deadlock-timeout does not honour
graceful-timeout: by the time it fires the GIL is wedged,
so the process exits forcibly via shutdown-timeout. The
setrlimit()-based memory-limit and
virtual-memory-limit are likewise hard kills outside
mod_wsgi’s control.
Avoid setting recycling triggers too aggressively. Constant restart adds load (every restart re-imports the application, warms caches, and may force a flurry of database connections), and short intervals can hide rather than expose the underlying problem. Use recycling as a safety net, not as a substitute for fixing leaks.
Timeouts
WSGIDaemonProcess exposes a family of timeout options.
This page gives a one-paragraph orientation for each;
Request Pipeline And Timeouts walks them through in the context of
the request flow they govern, including the recovery path
when one fires; and
WSGIDaemonProcess is the
per-option reference for behaviour, defaults, and edge cases.
The fault-recovery timeouts:
request-timeoutis a per-thread fail-safe for requests that block indefinitely. The fire point scales withthreadsby natural log so larger pools tolerate one wedge for longer before recovery kicks in.interrupt-timeout, when non-zero, attempts a thread-local recovery first by injecting amod_wsgi.RequestTimeoutexception into the wedged thread, restarting the process only if the injection does not unwind in time. This is the difference between losing one request (injection succeeds) and losing the whole pool’s worth of in-flight requests (injection times out).deadlock-timeoutcatches the case the injection mechanism cannot recover: a C extension holding the GIL across a blocking call. Detection is by absence of progress, recovery is process restart.
The shutdown timeouts:
graceful-timeoutis how long a process continues serving after a recycle trigger fires, waiting to reach idle. Also applied as the wait for in-flight requests whenrequest-timeoutcauses a forced recycle.eviction-timeoutis the corresponding wait when an operator sendsSIGUSR1directly to the daemon process (for example viapkill -USR1). Falls back tograceful-timeoutwhen not set.apachectl gracefuldoes not take this path: the Apache parent sendsSIGTERMto mod_wsgi daemons even on a graceful restart.shutdown-timeoutis the hard cutoff once shutdown is actually under way. The process is force-killed when this expires regardless of remaining state. The default of 5 seconds is enough for mostatexithandlers but short enough that recovery from a wedged process is prompt.startup-timeoutis the limit on how long the WSGI script can take to load. Forces a process restart when initial load hangs.
The transport timeouts (between Apache and the daemon process):
connect-timeoutis how long the Apache child waits for a successful connection to the daemon’s socket.socket-timeoutis the read/write timeout on the Apache-to-daemon socket connection. Falls back to Apache’sTimeoutdirective when not set.queue-timeoutis how long a request can wait in the daemon’s listen queue before being abandoned with 504. Useful for shedding load promptly when the pool is oversubscribed; without it requests pile up indefinitely.response-socket-timeoutis the timeout on flushes back to the client when the response buffer has filled.
The Request Pipeline And Timeouts guide reproduces the
mod_wsgi-express starter set of timeout values inline as a
baseline for hand-written WSGIDaemonProcess configurations.
Many of the timeouts are off by default, but the recommendation
is to set them explicitly so the pool can recover from
backlogging and hung requests.
The socket plumbing
Apache child processes communicate with daemon processes over
UNIX domain sockets. The WSGISocketPrefix directive sets the
directory and filename prefix for those sockets.
If WSGISocketPrefix is not set the sockets default to
Apache’s runtime directory. On some Linux distributions that
directory is permission-restricted in a way that prevents the
Apache child user from connecting to the socket; the symptom is
a 503 with WSGI0117 in the error log. The fix is to point
WSGISocketPrefix at a directory that the Apache child user
can read (the distribution-specific run/ location is the
usual choice):
WSGISocketPrefix run/wsgi
Do not place the sockets in /tmp. The directory must be
writable only by root (or by the user Apache is started as,
when not started as root). Anything more permissive is a
security exposure.
The same directory is also used for mutex lock files associated with daemon processes. See WSGIAcceptMutex for the related accept-mutex configuration.
When Apache is built with the mod_privileges module and
PrivilegesMode SECURE is in use, the user that the Apache
child process runs as while handling a request differs from
Apache’s normal child user. Use the socket-user= option on
WSGIDaemonProcess to set the socket owner to the user the
Apache child will be running as when it connects, otherwise
connection will fail with a permissions error. The same option
is needed for third-party Apache modules that change the child
user per-request (mod_ruid, mod_ruid2, mod_suid, the
ITK MPM).
Apache front-end MPM tuning
In daemon mode the Apache child workers do not run the
Python interpreter. They accept the client connection,
proxy the request to the daemon pool over a UNIX domain
socket, and relay the response back. Concurrency at the
Apache layer is therefore decoupled from the daemon pool’s
processes and threads settings: the Apache MPM
only needs enough capacity to keep all daemon workers
fed, plus headroom for queued connections.
Under a manually managed Apache, the relevant directives
are MaxRequestWorkers, StartServers,
MinSpare* / MaxSpare* for the active MPM, and
ServerLimit. See Processes And Threading for
the per-MPM walkthrough.
Under mod_wsgi-express (where daemon mode is the
default), the front-end MPM is sized automatically. The
defaults derive from --processes and --threads:
MaxRequestWorkers is set to
10 + max(10, int(1.5 * processes * threads)). The
four options that override these defaults:
--max-clients NUMBERTotal
MaxRequestWorkers. Defaults to the formula above. Override when the front-end needs more or less headroom than ~1.5x the daemon pool’s request concurrency.--max-clientsis silently raised to at leastprocesses * threadsif a smaller value is given, since a smaller value would block daemon workers.--initial-workers FRACTIONStartServersas a fraction of--max-clients. Defaults to 0.05 (5%) under prefork and 0.2 (20%) under worker / event.--minimum-spare-workers FRACTIONMinSpareServers(prefork) orMinSpareThreads(worker, event), as a fraction of--max-clients. Defaults to--initial-workersif not set.--maximum-spare-workers FRACTIONMaxSpareServers(prefork) orMaxSpareThreads(worker, event), as a fraction of--max-clients. Defaults to 0.1 (10%) under prefork and 0.6 (60%) under worker / event.
These options have no effect under --embedded-mode;
see “Apache MPM tuning” under “Embedded mode” above for
what happens there.
Operational visibility
By default daemon processes inherit Apache’s argv[0] (the
path to the httpd binary), making them indistinguishable
from the Apache parent and child processes in ps output.
Setting display-name=%{GROUP} renames each daemon process
to (wsgi:groupname) so it is clearly identifiable:
WSGIDaemonProcess myapp processes=2 threads=15 \
user=appsvc group=appsvc \
display-name=%{GROUP}
The rename is best-effort and constrained by the length of
Apache’s original argv[0]; the value may be truncated.
When WSGIDaemonProcess is declared inside a <VirtualHost>,
mod_wsgi log output for that pool is routed to the virtual
host’s ErrorLog rather than the main Apache error log. This
keeps per-pool error output co-located with the rest of the
virtual host’s logs and makes per-tenant log aggregation
straightforward.
Daemon-pool lifecycle events (start, recycle, shutdown,
abnormal exit) are logged at info and above; many internal
diagnostics are at debug. The LogLevel directive can be
scoped to mod_wsgi so the verbosity affects only mod_wsgi
output:
LogLevel warn wsgi:info
See Debugging Techniques for further log-output diagnostics.
Common pitfalls
C extensions that are not sub-interpreter-safe
Some C extensions assume they are loaded into the main Python
interpreter and crash or misbehave inside a sub interpreter.
NumPy, SciPy, and modules built on them are the prominent
examples. The fix is WSGIApplicationGroup %{GLOBAL} so the
application runs in the daemon process’s main interpreter
rather than a sub interpreter; see “Multiple Python Sub
Interpreters” in Application Issues.
Background threads
Application code can spawn its own threads, including at module import time. There is no fork-then-Python interaction to worry about: Python is initialised only in the daemon process (or, under embedded mode, in the Apache child workers), never in the Apache parent, so a thread started at import is started in the process that will run the application.
Two caveats apply.
Mark threads daemonic. Use threading.Thread(..., daemon=True).
Non-daemon threads block Python interpreter shutdown until they
exit, which can run into shutdown-timeout and force a hard
kill. For long-running tasks that need to stop cleanly on process
shutdown, register a mod_wsgi.subscribe_shutdown() callback
to signal the thread to exit; see Registering Cleanup Code.
Embedded mode plus WSGIScriptReloading On is hostile to
threads started inside the WSGI script file itself. In daemon
mode a WSGI-script reload triggers a process recycle, so any
threads started from the script die with the old process. In
embedded mode the reload is in-place: the script’s
sys.modules entry is dropped and the script is re-imported
in the same Apache child, so a thread started from the top of
the WSGI script gets started again on every reload, accumulating
in the process. If you need to start a background thread under
this configuration, start it from a regular Python module that
the WSGI script imports rather than from the WSGI script itself.
Imported modules are not dropped from sys.modules on reload,
so their module-level code (the thread start) does not
re-execute.
WSGIScriptReloading and signal-driven restart
Touching the WSGI script file triggers a daemon-process
restart via SIGINT. The restart goes straight to
shutdown-timeout without a graceful drain. If you need a
graceful drain on cutover, use SIGUSR1 directly and disable
WSGIScriptReloading to avoid racing the script-touch path
against the signal-driven restart. See
Upgrading An Application for the details.
Pool-name stability
A WSGIDaemonProcess declaration is read at Apache config
parse time. Renaming a pool requires editing
WSGIDaemonProcess and reloading Apache, which is a
heavier-weight operation than a graceful daemon restart. For
patterns that rotate which pool is “live” (blue/green
upgrades), keep the pool names stable across cycles and switch
the routing layer instead.
See Also
Processes And Threading for the Apache MPM and Python sub interpreter model that daemon mode builds on.
Security Hardening for the operational hardening of a daemon-mode deployment, including socket ownership and multi-tenant isolation.
Upgrading An Application for blue/green parallel pools and the cutover patterns.
Reloading Source Code for the daemon-mode code-reload mechanism.
Debugging Techniques for log-output diagnostics.
WSGIDaemonProcess, WSGIProcessGroup, WSGIApplicationGroup, WSGISocketPrefix, WSGIRestrictEmbedded, and WSGIRestrictProcess for the directive-level reference.