APScheduler. What is the best practices to shutdown schedulers with any non memory storage?
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
The correct way to shut down an APScheduler scheduler with a persistent job store (SQLAlchemy, MongoDB, Redis) is to call scheduler.shutdown(wait=True) inside a try/finally block or a signal handler. Setting wait=True ensures all currently running jobs finish before the scheduler stops, which prevents data corruption in the job store. For production applications, combine this with signal handling (SIGTERM, SIGINT) so that container orchestrators and process managers can trigger a clean shutdown.
Why Persistent Stores Require Careful Shutdown
When APScheduler uses an in-memory job store, an abrupt shutdown simply loses the job data. That is acceptable because in-memory jobs are ephemeral by design.
With persistent stores, the situation is different. The job store contains:
- Job definitions (function references, arguments, triggers)
- Next run times
- Job state (running, pending, paused)
If the scheduler process dies while a job is mid-execution and the store records the job as "running," the next process to start may see that job in an inconsistent state. Depending on the store and configuration, this can lead to:
- Jobs that never execute again because they appear permanently "running"
- Duplicate job executions if the store does not track running state
- Corrupted trigger data if a job was updating its next run time when the process was killed
scheduler.shutdown(wait=True) avoids these problems by letting running jobs complete and then cleanly disconnecting from the store.
Basic Shutdown Pattern
The simplest pattern uses try/finally:
The wait=True parameter (which is the default) blocks until all currently executing jobs finish. If you pass wait=False, the scheduler stops immediately without waiting, which is the same as an abrupt crash from the job store's perspective.
Signal-Based Shutdown for Production
In production, processes receive SIGTERM from container orchestrators (Kubernetes, Docker), systemd, or supervisord. Handle these signals explicitly:
The replace_existing=True parameter is important for persistent stores. Without it, restarting the application adds a duplicate job because the previous instance's job still exists in the database. With replace_existing=True, the existing job is updated rather than duplicated.
Handling Long-Running Jobs
If some jobs take a long time to complete, shutdown(wait=True) blocks indefinitely. There is no built-in timeout parameter on shutdown(). You can implement a timeout yourself:
For Kubernetes deployments, set the pod's terminationGracePeriodSeconds to a value longer than your longest-running job. Kubernetes sends SIGTERM first, then waits for the grace period before sending SIGKILL.
Using Event Listeners for Cleanup
APScheduler provides event hooks that let you track job lifecycle events. Use these to log and clean up during shutdown:
The EVENT_JOB_MISSED event is particularly important for persistent stores. If the scheduler was down during a job's scheduled time, APScheduler fires a misfire event on startup. Configure the misfire_grace_time to control how stale a missed run can be before it is skipped:
Comparison: Job Store Options
| Job Store | Backend | Persistence | Best For |
MemoryJobStore | In-process dict | None (lost on exit) | Development, testing |
SQLAlchemyJobStore | Any SQL database | Full | Production with existing SQL infrastructure |
MongoDBJobStore | MongoDB | Full | Document-oriented architectures |
RedisJobStore | Redis | Depends on Redis persistence | High-throughput, low-latency scheduling |
All persistent stores require the same shutdown discipline. The only difference is in connection cleanup. SQLAlchemy pools are closed by the store's shutdown() method. MongoDB and Redis connections are also released during shutdown.
AsyncIO Scheduler Shutdown
If you use AsyncIOScheduler, the shutdown pattern integrates with the asyncio event loop:
Flask and Django Integration
In web frameworks, the scheduler typically starts when the application starts and shuts down with the application:
The atexit handler ensures the scheduler shuts down when the Flask process exits. For production WSGI servers (Gunicorn, uWSGI), also handle SIGTERM as shown in the signal-based shutdown section, because atexit does not fire on SIGKILL.
Common Pitfalls
- Forgetting
replace_existing=True: Without this, every application restart adds a duplicate job to the persistent store. After 10 restarts, you have 10 copies of the same job running simultaneously. - Using
wait=Falseto avoid slow shutdowns: This defeats the purpose of graceful shutdown. If jobs take too long, implement a timeout wrapper rather than abandoning running jobs. - Not handling signals: In containerized environments,
SIGTERMis the primary shutdown mechanism. Without a signal handler, the process receivesSIGKILLafter the grace period, which is equivalent to pulling the power cord. - Running the scheduler in multiple worker processes: WSGI servers like Gunicorn spawn multiple workers. If each worker starts a scheduler, you get N copies of every job. Use
--workers 1for the scheduler process, or run it as a separate service. - Ignoring misfire events: If the scheduler was down for an hour and a job runs every 5 minutes, there are 12 missed runs. Without
misfire_grace_time, APScheduler may try to execute all 12 at once on startup, depending on thecoalescingsetting. - Not logging shutdown events: When troubleshooting "why did jobs stop running," having log entries for scheduler startup and shutdown is invaluable. Always log the shutdown reason (signal number, exception, or normal exit).
Summary
- Always call
scheduler.shutdown(wait=True)in atry/finallyblock or signal handler when using persistent job stores. - Use
replace_existing=Truewhen adding jobs to prevent duplicates after application restarts. - Handle
SIGTERMandSIGINTfor clean shutdown in containerized and daemon deployments. - Set
misfire_grace_timeto control behavior for jobs missed during downtime. - For long-running jobs, implement a timeout wrapper around
shutdown(wait=True)and align it with your container's termination grace period. - Register the scheduler shutdown with
atexitin web frameworks, but also handle signals for production WSGI deployments.
Related reading
- Are Boto3 Resources and Clients Equivalent? When Use One or Other?
- Are dictionaries ordered in Python 3.6?
- Are list-comprehensions and functional functions faster than for loops?
- Are locks unnecessary in multi-threaded Python code because of the GIL?
- Are nested try/except blocks in Python a good programming practice?
- Are tuples more efficient than lists in Python?
- .arff files with scikit-learn?
- argparse identify which subparser was used
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.