Oct 27, 20265 min read

How to Add Cron Jobs to a Flask App

Scheduling a daily report from a Flask app with Encore, Flask-APScheduler, rq-scheduler or Google Cloud Scheduler, with jobs that are safe to run twice.

Flask has no scheduler, so recurring work like a daily report or a cleanup ends up in a thread inside the web process, a separate worker, or a crontab on one server. Each choice decides how many times the job runs once the app scales past one process.

This guide schedules a daily signup report in a Flask app with Encore, Flask-APScheduler, rq-scheduler and Google Cloud Scheduler. Each version runs at 06:00 UTC and is safe to run twice for the same day.

What you need to decide:

  • Where the schedule is defined and reviewed.
  • How many processes run the job when the app scales.
  • How a failed or doubled run is handled.
  • How you trigger the job during development.

Cron options for Flask at a glance

CapabilityEncoreFlask-APSchedulerrq-schedulerCloud Scheduler
Schedule defined inPython next to the routeFlask configPython, stored in RedisGCP, outside the repo
Runs per scheduleOnceOnce per processOnce with one schedulerOnce
Extra processesNoneNoneRedis, scheduler and workerNone
Local testingPOST to the routeRuns in the dev serverRun Redis and both processesCall the route by hand
ProductionManaged schedule in your AWS or GCP accountTied to web processesYou run all threeGCP only

Encore: cron jobs for your existing Flask app

Encore

Encore combines scheduled jobs and the other infrastructure in your Flask code with a local environment and a deploy to your own AWS or GCP account, where a managed scheduler calls each job. It is an infrastructure SDK added to an existing app, and the routes stay ordinary Flask routes.

Encore declares infrastructure in Python code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in Python, running locally under encore run and provisioned in your own AWS or GCP account on deploy.

A cron job names the path to call and the schedule. When it is due, Encore sends a POST request to that path:

from encore import cron from flask import Flask app = Flask(__name__) # Encore sends POST /internal/daily-report to this service every day at 06:00 UTC. daily_report = cron.Job( "daily-report", path="/internal/daily-report", title="Email yesterday's signup report", schedule="0 6 * * *", )

The route keys the run on the day it reports, so a retried or doubled call does nothing. The set stands in for a table with a unique date column:

from datetime import date, timedelta sent_reports: set[str] = set() # Stand-in for a table keyed by report date. @app.post("/internal/daily-report") def send_daily_report(): day = (date.today() - timedelta(days=1)).isoformat() # A retried or doubled run for the same day does nothing. if day in sent_reports: return {"day": day, "sent": False} sent_reports.add(day) return {"day": day, "sent": True}

The app is served with Waitress on the address Encore assigns:

import os from waitress import serve if __name__ == "__main__": host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "0.0.0.0:8000").rpartition(":") serve(app, host=host or "0.0.0.0", port=int(port))

Cron jobs do not fire on a schedule under encore run, so you test by calling the route. The second call for the same day reports sent: false:

~/reports
$ encore run$ curl -X POST localhost:4000/internal/daily-report{"day": "2026-10-26", "sent": true}$ curl -X POST localhost:4000/internal/daily-report{"day": "2026-10-26", "sent": false}

On deploy, the schedule is created in your own cloud account and each run gets a trace, so a failed report shows the request that failed. Adding every="1h" instead of schedule gives a fixed interval.

When an agent adds a job, the schedule, the route and the test request sit in one file, and the deploy creates the schedule in the cloud.

Good to know

The scheduler calls one instance per run, so the job does not multiply as the service scales. Schedules are in UTC. Keep the work short or hand long work to a queue, so a run finishes before the next one starts.

Try Encore →

Flask-APScheduler

Flask-APScheduler runs APScheduler inside the Flask process and reads jobs from config:

from flask import Flask from flask_apscheduler import APScheduler class Config: JOBS = [ { "id": "daily-report", "func": "reports.jobs:send_daily_report", "trigger": "cron", "hour": 6, } ] app = Flask(__name__) app.config.from_object(Config()) scheduler = APScheduler() scheduler.init_app(app) scheduler.start()

Good to know

Every process that imports the app starts a scheduler, so four workers send four reports. Production setups run it in a separate single process, and jobs stop when that process restarts or scales to zero.

rq-scheduler

rq-scheduler stores schedules in Redis and enqueues jobs for RQ workers:

from redis import Redis from rq_scheduler import Scheduler from reports.jobs import send_daily_report scheduler = Scheduler(connection=Redis.from_url("redis://localhost:6379")) scheduler.cron("0 6 * * *", func=send_daily_report, id="daily-report")

Good to know

You run Redis, one rqscheduler process and RQ workers. Schedules live in Redis rather than the repository, so the registration code runs on each deploy and needs a stable id to avoid duplicates.

Google Cloud Scheduler

Cloud Scheduler calls an HTTP endpoint on a cron schedule:

gcloud scheduler jobs create http daily-report \ --schedule="0 6 * * *" --time-zone=UTC \ --uri="https://reports-abc123-ew.a.run.app/internal/daily-report" \ --http-method=POST \ --oidc-service-account-email="scheduler@$PROJECT.iam.gserviceaccount.com"

Good to know

The scheduler account needs the Cloud Run Invoker role on the service, and the route should check the OIDC token. The schedule lives in GCP rather than the code, and nothing runs it during local development.

How to choose

  • Encore if you want the schedule in the repository, one run per tick and the scheduler created in your own AWS or GCP account.
  • Flask-APScheduler for a single long-lived process.
  • rq-scheduler if the app already runs RQ and Redis.
  • Cloud Scheduler for a Flask app on Cloud Run where schedules are managed in GCP.

Python support in Encore is in beta, and you can sign up for access to try it with your Flask app.

Frequently asked questions

How do I run a cron job in a Flask app?

Expose the job as a route that is safe to run twice and have one scheduler call it. With Encore, declare cron.Job("daily-report", path="/internal/daily-report", title="Email yesterday's signup report", schedule="0 6 * * *") next to a Flask route on that path. After deploy, Encore calls it on schedule in your AWS or GCP account.

Why does my Flask APScheduler job run more than once?

Each Gunicorn or Waitress process that imports the app starts its own scheduler. With several workers or instances, each one runs the job. Run the scheduler in one dedicated process or use an external scheduler.

How do I test a Flask cron job locally?

Call the job's route directly. With Encore, cron jobs do not fire on a schedule under encore run, so you POST to the path while the app runs locally and check the result.

How do I make a scheduled job idempotent?

Key each run on the period it covers, such as the report date, and record it under a unique constraint. A retried or doubled run for the same period finds the record and does nothing.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders