# ADR-004: APScheduler over Celery ## Status Accepted ## Context BanGUI requires background task scheduling for periodic work: geo cache flush, session cleanup, history sync, and blocklist imports. ## Decision Use **APScheduler 4.x (AsyncIOScheduler)** for background scheduling. ## Rationale ### Why APScheduler over Celery? - **No infrastructure:** Celery requires a message broker (Redis or RabbitMQ). APScheduler runs in-process with no broker. Given BanGUI's single-instance constraint, a message queue adds unnecessary operational complexity. - **Async-native:** `AsyncIOScheduler` integrates directly with the asyncio event loop. All BanGUI's I/O (database, HTTP, fail2ban socket) is async. APScheduler jobs are `async def` functions that `await` without blocking. - **Simplicity:** BanGUI's job set is fixed and small. Celery's rich task routing, retry policies, and distributed execution are overkill. APScheduler covers cron-style scheduling with simpler semantics. - **Single-instance enforcement:** APScheduler's in-memory job store is a natural fit when there is only one scheduler. No distributed coordination needed. ### Why not Celery? - Celery's architecture (broker + workers + result backend) is designed for distributed systems. BanGUI is explicitly single-instance. - Celery tasks are synchronous wrappers around async code without careful handling. Native `async def` tasks require `async_task()` or explicit `run_sync`, creating friction in an async-first codebase. - Added operational burden: Redis or RabbitMQ must be available at startup. ### Trade-offs - **No horizontal scaling of workers:** APScheduler jobs run in the single uvicorn worker process. CPU-intensive jobs would block the event loop. (This is not a concern for BanGUI's I/O-bound jobs.) - **No built-in retry mechanism:** Failed jobs must re-raise exceptions or implement retry logic manually. This is acceptable given BanGUI's job idempotency guarantees. ## Consequences - Scheduler is configured in `app/startup.py` using `AsyncIOScheduler`. - Jobs live in `app/tasks/`. - Single-worker constraint is enforced via `BANGUI_WORKERS=1` validation and the `scheduler_lock` database table.