Kickmates scores football predictions the moment real matches finish. That means three time-sensitive systems running at once: a rate-limited external API, a background scoring engine, and a real-time push layer. The obvious tool is Celery. I didn't reach for it.
The constraint
API-Football allows 90 calls per minute. A naive poll-everything loop would burn the quota in hours or score predictions minutes late — either one kills the live feel the product is built on.
The decision
Celery is correct at scale, but it adds a broker, a worker container, and operational weight the product didn't need yet. I embedded APScheduler in FastAPI's lifespan with a tiered schedule:
- Fixture statuses sync once daily at 11 PM UTC
- Scoring runs every 50 minutes, targeting only
is_scored = Falsematches - Reminders fire hourly
- Gameweek advancement runs nightly at 1 AM
A sliding-window rate limiter with async locking keeps concurrent jobs from racing to the same quota.
The trade-off
APScheduler is single-instance. If the server restarts mid-job, the scheduler doesn't survive without coordination logic. Acceptable here: missed windows recover on the next 50-minute cycle with no data loss, and the current scale doesn't justify Celery's overhead.
The lesson isn't "never use Celery." It's: pick the tool that fits the constraint in front of you, not the one that fits a scale you don't have yet.