How to setup periodic task in Celery, Django?

The TLDR answer:

If you using celery for years, then you must have got used to the @periodic_task decorator to help you set up any periodic task. But as soon as you update your celery, you get a deprecation warning, or else if you start using the latest version of celery, you will no more find the periodic task in the package guide. There are a lot of weird (both good and bad) ways to set up periodic tasks. But none of them give you the comfort of using a decorator, you were using in the past.

If you need help to set up Celery with Django, head on to the following blog.
How to setup Celery with Django?
Celery is an asynchronous task framework that can be used to perform long-running jobs required in the Django web application

I found the following solution closed to decorator usage.

# task.py, this file can be present in any of the apps.
from celery.schedules import crontab

from backend.celery import app
from backend.celery import task


@app.on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):
    # Call every 2 hours 52 min.
    sender.add_periodic_task(
        crontab(minute=52, hour="*/2"),
        test.s("world")
    )


@task
def test(arg):
    print(arg)
# celery.py, this file should be present in the setting.py folder level

from celery import Celery

app = Celery("backend") # "backend" is your main application name, i.e. your folder name where you have this file
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

task = app.task

You can read more about this from the official documentation.

Note: For someone who doesn't know how to set up periodic tasks, please keep on reading, where I have explained what does celery beat mean and how to integrate it into your project.

The Detailed Answer:

I am assuming you already have a basic understanding of celery and how to set up Celery with the Django project.

For running any periodic task we need a celery beat service that will maintain all the schedules and then pass them on to the main celery worker for execution. Do remember that we can have only one instance celery beat worker running, else we will see duplicate periodic tasks running in our project.

One interesting fact which most beginners might miss is, celery beat would only initiate the task at the specified time, i.e. it would put it in the queue when the scheduled time for execution arrives. You need your celery worker running to pick the task from the queue and then execute it.

If you already have celery setup in the project using docker, then all you need to do is, add a new service in your docker-compose.yaml

celery-beat:
    build: ./backend
    command: celery -A backend beat -l info
    volumes:
      - ./backend/:/app/
    env_file:
      - ./.env
    depends_on:
      - postgresql_db
      - redis_server

You can get the full reference of the setup from the example project.

arghyaiitb/django-celery-postgres-redis-react-github-aws
An example web app build with django, celery, react, postgres and redis. - arghyaiitb/django-celery-postgres-redis-react-github-aws

Once you have the setup, all you need to do is have the periodic task setup. Use the below snippet to register the tasks.

# task.py, this file can be present in any of the apps.
from celery.schedules import crontab

from backend.celery import app
from backend.celery import task


@app.on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):
    # Call every 2 hours 52 min.
    sender.add_periodic_task(
        crontab(minute=52, hour="*/2"),
        test.s("world")
    )

@task
def test(arg):
    print(arg)

The most important thing in the whole setup is @app.on_after_finalize.connect. What does this mean?

As per the documentation, it mentions that we should use @app.on_after_configure.connect but for Django projects, this doesn't work. You need to use a different signal @app.on_after_finalize.connect.  Do make a note of the subtle difference between configure and finalize.

Periodic Tasks — Celery 5.0.5 documentation

As per the documentation

on_after_configure
Signal sent after app has prepared the configuration.
on_after_finalize
Signal sent after app has been finalized.

For any Django project modules get loaded only when the app is ready. This signal will be fired after all tasks.py are imported and all possible receivers are already subscribed.