How python graceful shutdown works in StayPresent — SIGINT/SIGTERM handling, install_signal_handlers, and chaining your own signal handler.
Graceful Shutdown and Signal Handling in StayPresent
Deploy platforms and container orchestrators don't just kill your process when they want it to stop — they send a signal first and expect your application to clean up. A bot that ignores SIGTERM risks corrupted state, orphaned subprocesses, or a database write cut off mid-transaction. This guide covers exactly how python graceful shutdown works inside StayPresent, and how to hook your own cleanup logic into it.
Table of Contents
- Why Signals Matter for Deployment
- What Happens on SIGINT/SIGTERM by Default
- Shutdown During a Restart Backoff
- Chaining Your Own Signal Handler
- Disabling StayPresent's Signal Handling
- Signals and the Main Thread Requirement
- Multi-Bot Shutdown Behavior
- Full Example
- Best Practices
- Common Mistakes
- FAQs
- Conclusion
Why Signals Matter for Deployment
When Render, Railway, Docker, or systemctl stop needs your process to shut down — for a redeploy, a scale-down event, or a manual stop — the standard mechanism is a SIGTERM signal, giving the process a window to shut down cleanly before a harder SIGKILL follows. Ctrl+C in a terminal sends SIGINT, the interactive equivalent. Handling both properly is what separates "the bot stopped" from "the bot stopped correctly."
What Happens on SIGINT/SIGTERM by Default
By default (install_signal_handlers=True, StayPresent's own default), receiving SIGINT or SIGTERM triggers a clean, coordinated teardown:
import staypresent
staypresent.run("bot.py")
# Ctrl+C -> web server and bot process both shut down cleanly
Both the web server and every running bot subprocess are terminated as part of this sequence — not just one or the other.
Shutdown During a Restart Backoff
There's a subtle race condition this has to handle correctly: what if a signal arrives while a crashed bot is sitting in its restart_delay backoff, waiting to respawn? StayPresent synchronizes shutdown and the pending respawn so that exactly one of them wins:
- Either the respawn is cancelled before it happens, or
- Shutdown terminates the freshly-respawned process too.
Either way, a shutdown can never leave an untracked, un-terminated bot process running in the background — which is exactly the kind of leak that's easy to introduce with hand-rolled process supervision and hard to notice until your host's process count creeps up over time.
The restart_delay wait itself is also interruptible — a signal arriving during it wakes the bot's monitor thread immediately, rather than shutdown being held up waiting out the full backoff period.
Chaining Your Own Signal Handler
If your own script already registers a handler for SIGINT/SIGTERM before calling run(), StayPresent doesn't silently discard it:
import signal
import staypresent
def my_cleanup(signum, frame):
print("Flushing local cache before shutdown...")
signal.signal(signal.SIGTERM, my_cleanup)
staypresent.run("bot.py")
# On SIGTERM: StayPresent's own cleanup runs first (terminating bot
# processes, logging active cron pingers), THEN my_cleanup runs.
This ordering matters: StayPresent's own teardown (bot processes, server, cron pinger visibility) happens first, and your previously-installed handler is called afterward — so your cleanup code can safely assume the bots are already stopped by the time it runs.
Disabling StayPresent's Signal Handling
Set install_signal_handlers=False if your script wants full, exclusive control over shutdown signaling — for example, if you're embedding staypresent.run() inside a larger application that already has its own signal-handling framework:
staypresent.run(
"bot.py",
install_signal_handlers=False,
)
With this set, StayPresent won't install any SIGINT/SIGTERM handling of its own at all — shutdown behavior becomes entirely whatever your own code implements.
Signals and the Main Thread Requirement
Python signal handlers can only be registered on the main thread — this is a Python/OS-level constraint, not something StayPresent can work around. If run() is called from a non-main thread, a warning is logged and graceful signal handling is simply skipped rather than raising an exception:
import threading
import staypresent
def start():
staypresent.run("bot.py") # WARNING logged, signal handling skipped
threading.Thread(target=start).start()
Multi-Bot Shutdown Behavior
With multiple bots (via bot_file as a list, bot_module as a list, or bots), a SIGINT/SIGTERM terminates the web server and every running bot process, not just one. Each bot's own crash-recovery-vs-shutdown race is handled independently and safely, using the same synchronization described above.
Full Example
import signal
import staypresent
def notify_shutdown(signum, frame):
print("Bot(s) shutting down, notifying monitoring...")
signal.signal(signal.SIGTERM, notify_shutdown)
signal.signal(signal.SIGINT, notify_shutdown)
staypresent.web.json({"status": "running"})
staypresent.run(
bots=[
{"file": "telegram_bot.py"},
{"file": "worker.py"},
],
restart_on_crash=True,
)
Best Practices
- Register your own signal handlers before calling
staypresent.run(), so they get chained rather than needing to be re-registered afterward. - Keep chained cleanup handlers fast — a slow handler delays how quickly the process actually exits after StayPresent's own teardown finishes.
- Only set
install_signal_handlers=Falseif you have a concrete reason to manage signals yourself; the default handles the bot-process-and-server coordination correctly out of the box.
Common Mistakes
-
Registering a signal handler after calling
run(). StayPresent only chains handlers that were already installed beforerun()starts — anything registered afterward simply replaces StayPresent's own handler rather than being chained to it. -
Assuming
install_signal_handlers=Falsemeans "no shutdown happens." It just means StayPresent won't install its own signal handling — your own code is then fully responsible for triggering and coordinating a clean shutdown. -
Calling
run()from a background thread and being surprised graceful shutdown doesn't work. This is a hard Python constraint on where signal handlers can be registered, not a bug — callrun()from the main thread if you need signal handling.
FAQs
Does SIGKILL trigger graceful shutdown?
No — SIGKILL can't be caught by any process, StayPresent included. It terminates immediately with no cleanup opportunity at all, which is why platforms typically send SIGTERM first and only escalate to SIGKILL after a grace period.
What happens to cron() pingers on shutdown?
They run on daemon threads and aren't explicitly stopped by run()'s shutdown sequence — they're simply logged for visibility and then torn down automatically when the process exits, since daemon threads don't block process exit.
Can I trigger the same shutdown sequence programmatically, without an actual OS signal?
Not directly — shutdown is driven by the signal handler itself; sending your own process a real SIGTERM (e.g. via os.kill(os.getpid(), signal.SIGTERM)) is the supported way to trigger it from within your own code.
Conclusion
Python graceful shutdown isn't just about catching Ctrl+C — it's about correctly coordinating a signal against an in-flight restart, terminating every bot process (not just one), and giving your own cleanup code a reliable place to run. StayPresent handles all three by default, while still leaving room to chain your own handler or opt out entirely.
pip install staypresent[prod]
Top comments (0)