Back to Blog
Python

Python Daemon Thread: Behavior, Use Cases, and Pitfalls

python daemon thread: Learn how Python daemon threads behave at interpreter exit, when to use them for background work, and the pitfalls of abrupt termination.

threadingdaemon threadsconcurrencybackground tasksthread lifecycle
Illustration of a Python daemon thread running in the background while the main program exits without waiting for it.

A python daemon thread is a threading.Thread whose daemon attribute is True. The defining behavior is simple: when only daemon threads are still running, the Python interpreter exits immediately without waiting for them. That makes daemon threads useful for background work that must not delay program shutdown, but it also means they can be killed at any moment, with no guarantee that cleanup code runs.

What a Daemon Thread Is and How to Create One

The threading module exposes the daemon flag on every Thread instance. You can set it in the constructor:

import threading import time def monitor(): while True: print("monitoring...") time.sleep(1) t = threading.Thread(target=monitor, daemon=True) t.start()

You can also set it after construction but before start():

t = threading.Thread(target=monitor) t.daemon = True t.start()

The flag must be set before the thread starts. Assigning daemon after start() raises RuntimeError. The value is inherited from the creating thread: a daemon thread creates daemon child threads by default, and a non-daemon thread creates non-daemon children.

What Happens When Only Daemon Threads Remain

The interpreter's shutdown sequence checks for running non-daemon threads. As long as at least one non-daemon thread is alive, the process keeps running. When the main thread finishes and every remaining thread is a daemon, the interpreter begins shutdown and terminates the daemon threads without joining them.

This is the key difference from a regular thread. A non-daemon thread blocks exit until its target function returns. A daemon thread does not. Consider:

import threading import time def worker(): time.sleep(5) print("worker finished") t = threading.Thread(target=worker, daemon=True) t.start() print("main exiting")

With daemon=True, the program prints main exiting and exits immediately. The worker finished line never appears because the thread is killed during shutdown. With daemon=False, the program waits roughly five seconds, prints worker finished, then exits.

Choosing Between Daemon and Non-Daemon Threads

The decision depends on whether the program must wait for the thread's work to complete.

CriterionDaemon threadNon-daemon thread
Blocks interpreter exitNoYes
Cleanup guaranteed at exitNoYes, if the target returns normally
Suitable forBackground monitoring, heartbeats, periodic cleanupWork that must finish before the process ends
Failure modeThread killed mid-operationThread runs to completion or hangs the process

Use a daemon thread when the work is optional and losing it at exit is acceptable. A health-check loop that reports metrics is a good candidate: if the process is shutting down, the final metric report does not matter. Use a non-daemon thread when the work must complete, such as flushing a buffer, writing a final log record, or closing a network connection.

Why Daemon Threads Are Terminated Abruptly

Because the interpreter does not join daemon threads, they are stopped in the middle of whatever they are doing. A finally block is not a reliable place to clean up in a daemon thread. If the thread is executing time.sleep() or waiting on an I/O operation when shutdown begins, the interpreter terminates it without unwinding the stack in the usual way.

This matters for any resource the daemon thread holds. A file handle opened inside a daemon thread may not be flushed. A database connection may not be closed. A lock held by a daemon thread at exit can leave other threads blocked during shutdown, because the interpreter does not release locks held by abruptly terminated threads.

If a background task needs to release resources on exit, a non-daemon thread with an explicit shutdown signal is safer. The thread can check the signal, perform cleanup, and return normally.

Setting the Daemon Flag Before Starting

The ordering constraint is easy to miss. The daemon attribute is only writable before the thread begins execution. After start() is called, any assignment raises RuntimeError:

t = threading.Thread(target=worker) t.start() t.daemon = True # RuntimeError: cannot set daemon status of active thread

The same constraint applies to the constructor. If you pass daemon=True to Thread(), the flag is set at construction time, which is always before start(). The practical rule is to decide the daemon status when the thread is created, not after it is running.

Shared State and Exit Races

Daemon threads run concurrently with the main thread and with other threads, so they see the same shared objects. The GIL serializes bytecode execution, but it does not make operations atomic at the application level. A daemon thread can be terminated between any two bytecode instructions, which means a partially updated data structure can be left behind.

This is most visible during shutdown. If the main thread is writing to a shared list while a daemon thread reads it, the daemon thread can be killed mid-read. The interpreter does not coordinate this. For background tasks that mutate shared state, use a threading.Lock around the critical section, and accept that the daemon thread may still be killed while holding the lock.

A Practical Background Monitor Example

A common use is a background monitor that records resource usage while the main program does its work:

import threading import time class ResourceMonitor: def __init__(self): self._stop = threading.Event() self.samples = [] self._thread = threading.Thread(target=self._run, daemon=True) def start(self): self._thread.start() def stop(self): self._stop.set() def _run(self): while not self._stop.is_set(): self.samples.append(time.time()) time.sleep(0.5) monitor = ResourceMonitor() monitor.start() # main work time.sleep(3) monitor.stop() print(f"collected {len(monitor.samples)} samples")

The daemon thread keeps sampling until stop() is called. If the main program exits without calling stop(), the daemon thread is killed and the samples already collected are still available, because the list is shared. The Event gives the thread a way to exit cleanly when the program has time to stop it, while the daemon flag guarantees the program can exit even if stop() is never reached.

This pattern works well for telemetry, cache warming, and periodic maintenance tasks where a lost final iteration is acceptable. For work that must survive until completion, a non-daemon thread with the same Event-based shutdown is the correct choice.

python daemon thread: Practical Usage and Code Examples | RYUSLOG DEV