Understanding daemon vs non-daemon threads
Daemon vs non-daemon is one of those distinctions that seems trivial ("just a boolean flag") until you get bitten by it, and then you never forget it.
The core difference
The JVM has one simple rule for when to shut down: the JVM exits when all non-daemon threads have finished. Daemon threads are ignored in that calculation entirely.
That is it. That is the whole distinction. Everything else follows from that one rule.
Put another way: non-daemon threads keep the JVM alive. Daemon threads do not. When the last non-daemon thread finishes, the JVM stops caring what daemon threads are doing and terminates the process; mid-execution, mid-loop, mid-I/O, whatever.
By default, all threads are non-daemon. The main thread is non-daemon, and any thread you create inherits its parent's daemon status. So threads started from main are also non-daemon unless you explicitly say otherwise.
A concrete demo of the rule
Say you write this:
fun main() {
val worker = Thread {
repeat(10) {
println("Working... $it")
Thread.sleep(500)
}
println("Done!")
}
worker.apply {
isDaemon = false // non-daemon (the default)
start()
}
println("main() returning")
}
main() returns almost immediately. But you will see all 10 "Working..." lines and "Done!" print, and then the JVM exits. Why? Because the worker thread is non-daemon, and the JVM waits for it before shutting down.
Now flip one line:
worker.isDaemon = true // daemon
Now main() returns, prints "main() returning", and the JVM immediately terminates; you will typically see zero "Working..." lines, or maybe one or two if you are lucky with timing. The daemon worker is killed mid-execution because nothing was keeping the JVM alive.
When each one matters
Non-daemon (the default): Use when losing the work would be a problem.
Your actual application work should be non-daemon. If a user tapped "Send message" and your app is halfway through the HTTP POST, you do not want the JVM to exit and drop the request just because main() (or on Android, some other lifecycle callback) happened to return. The whole point of "waiting for work to finish" is what non-daemon threads give you.
This is why the general-purpose networking pool should have non-daemon threads. If a user initiates a request, your library owes them either a response or a failure; silently vanishing because the process exited is the worst possible outcome.
Daemon: Use when the work is optional/best-effort/background-only.
Daemon threads make sense when the work is conceptually subservient to the main application. If the main app is done, this work is by definition irrelevant, and there is no harm in killing it.
Scenario 1: Analytics uploader
You have an analytics library that batches events and uploads them every 30 seconds. If the user closes the app while a batch is mid-upload, what should happen?
If the uploader is non-daemon: the JVM will stubbornly wait for the current upload to finish before actually exiting. From the user's perspective, they closed the app but the process is still there, using battery, holding a socket, until the analytics call completes. If the network is slow, this could be seconds. If the endpoint is down and the request is stuck waiting for a timeout, this could be minutes. Worse, if the uploader has a polling loop that runs forever, the JVM never exits. Every 30 seconds, forever, until the OS force-kills the process.
If the uploader is daemon: when the app closes, the uploader is killed mid-upload, and the JVM exits cleanly. The in-flight batch is lost; but that is the right tradeoff for analytics. Losing one batch of "user tapped button X" events is fine; blocking the user's app shutdown to preserve them is not.
The principle: the loss of the work is less bad than the cost of waiting for it. For analytics, that is almost always true.
Scenario 2: Best-effort telemetry / heartbeats
Your app pings a health-check endpoint every 60 seconds so your backend knows the client is alive. Same reasoning: if the app is shutting down, the client is by definition no longer alive, and sending one last heartbeat is pointless. You want that thread to just die when the app dies. Daemon is the right call.
Scenario 3: File writer
Suppose you have a background thread that writes the user's document to disk. Should it be daemon?
Absolutely not. If the JVM exits mid-write, the file is corrupted and the user's work is lost. This thread must be non-daemon, so the JVM waits for the write to complete before exiting.
General decision heuristic
Ask yourself: If the JVM exits right now while this thread is mid-task, is that bad?
- Yes, bad → non-daemon. (User work, financial transactions, writes to persistent storage, in-flight API calls that the user initiated.)
- No, fine → daemon. (Analytics, telemetry, cache warmers, background metric collection, prefetch that will just re-run next launch.)
The wrinkle on Android specifically
On Android, the JVM's "exit when all non-daemon threads finish" rule technically still applies, but in practice the Android runtime keeps your process alive based on component lifecycle (Activities, Services, foreground state), not thread counts. The system can also kill your process at any time regardless of what threads are running.
So on Android, the daemon flag matters less for JVM shutdown and more for a related concern: whether a lingering non-daemon thread prevents your process from being cleanly reclaimable. A rogue non-daemon thread pool that keeps threads alive after your Activity is destroyed can leak the entire Activity (and its context, view hierarchy, and everything else) until GC. Making pools daemon-flagged when their work is truly disposable helps the runtime clean up more predictably.
But the general principle still holds: user-initiated work → non-daemon; background/optional work → daemon.
Top comments (0)