Skip to content

Notifications — Tasks

Overview

The notifications.tasks package contains asynchronous and scheduled execution entry points for the notifications app.

These tasks are responsible for:

  • processing pending notification deliveries
  • reconciling Expo push receipts
  • firing scheduled notifications
  • cleaning up old notifications
  • deactivating stale push devices

The task layer is intentionally thin.

Its primary role is to:

  • run work asynchronously
  • call service-layer functions
  • return summary results

Business logic should live in services, not in tasks.


Responsibilities

The task layer is responsible for:

  • asynchronous execution via Celery
  • scheduled execution of due work
  • safe batch processing
  • wrapping service functions for workers/beat
  • returning operational summaries

It is not responsible for:

  • notification creation rules
  • preference resolution
  • payload rendering
  • provider-specific business logic
  • delivery modeling

Main tasks

process_pending_deliveries

Processes pending NotificationDelivery rows that are due.

Typical responsibilities:

  • select pending delivery rows
  • lock rows safely with select_for_update(skip_locked=True)
  • hand off sending to the delivery service
  • update delivery status and retry scheduling
  • return counts for:
  • sent
  • failed
  • retried
  • skipped

This task is the main async executor for push delivery.


check_expo_push_receipts

Polls Expo for final receipt status of previously accepted push tickets.

Typical responsibilities:

  • find unchecked ExpoPushTicket rows that are due
  • query Expo receipts API
  • mark tickets checked when terminal
  • retry missing receipts with backoff
  • deactivate invalid devices when Expo reports DeviceNotRegistered

This task exists because Expo delivery is asynchronous and ticket acceptance does not guarantee final delivery.


run_scheduled_notifications

Fires due ScheduledNotification rows.

Typical responsibilities:

  • find active rows with run_at <= now
  • call notification services to create actual notifications
  • deactivate the scheduled row after successful execution
  • return the number of fired rows

This task converts future schedules into real notifications.


cleanup_notifications_task

Deletes cleared notifications older than the configured retention window.

Typical responsibilities:

  • call cleanup service
  • remove soft-deleted rows after retention
  • return number of deleted notifications

This keeps the inbox storage bounded over time.


cleanup_stale_push_devices_task

Deactivates push devices that have not been seen recently.

Typical responsibilities:

  • find active devices with stale last_seen_at
  • mark them inactive
  • return the number of deactivated rows

This helps keep push delivery targeting accurate.


Task-to-service relationship

The tasks package should remain thin and delegate to services.

flowchart TD
    A[Celery Task] --> B[Notifications Service]
    B --> C[Models]
    B --> D[Providers]
    B --> E[Retry / status updates]

Examples

  • process_pending_deliveries -> delivery service
  • check_expo_push_receipts -> Expo receipt service
  • run_scheduled_notifications -> scheduled notification service
  • cleanup_notifications_task -> cleanup service

Batch processing model

Most tasks work in batches.

This is important for:

  • predictable worker runtime
  • safe retry behavior
  • reduced DB lock duration
  • scalability under load

Typical patterns include:

  • batch_size parameters
  • ordering by oldest rows first
  • due-time filtering
  • summary dict return values

Concurrency and locking

Some tasks use database row locking for correctness.

Delivery processing

process_pending_deliveries should lock rows with:

  • select_for_update(skip_locked=True)

This allows multiple workers to run concurrently without processing the same delivery twice.

Expo receipt checking

check_expo_push_receipts should also use:

  • select_for_update(skip_locked=True)

This prevents duplicate reconciliation work.


Retry behavior

Retry behavior belongs to the execution pipeline, not to views.

Delivery retries

Pending deliveries may be retried when:

  • provider call fails
  • rate limiting occurs
  • temporary transport errors occur

Retry timing is controlled by config helpers such as:

  • backoff calculation
  • rate-limit retry delay
  • max attempts

Receipt retries

Expo receipts may be missing temporarily.

The task tracks:

  • missing_attempts
  • next_check_at

After a configured maximum number of missing checks, the ticket is marked terminal as missing.


Provider interaction

Tasks do not talk to providers directly unless they are the async execution boundary for a provider-backed workflow.

Examples:

  • delivery task uses push sender abstraction
  • Expo receipts task uses Expo receipts client abstraction

This keeps provider-specific code isolated behind interfaces and helper clients.


Operational summaries

Tasks generally return compact summary dicts.

Delivery processing

  • sent
  • failed
  • retried
  • skipped

Expo receipts

  • checked
  • deactivated
  • missing

Scheduled notifications

  • fired

Cleanup

  • deleted_notifications
  • deactivated

These summaries are useful for:

  • logs
  • manual runs
  • monitoring
  • tests

Safety guarantees

The task layer should preserve these guarantees:

  • no double-processing of locked rows
  • retries remain bounded
  • invalid tokens are deactivated
  • scheduled rows are not fired repeatedly
  • cleanup is idempotent

What tasks do not do

Tasks do not:

  • decide user preference rules
  • render templates directly
  • create registry definitions
  • expose API responses
  • replace services as the source of truth

Those responsibilities belong to:

  • services
  • providers
  • serializers
  • views

Best practices

  • keep tasks thin and delegate to services
  • use row locking for queue-like processing
  • process work in bounded batches
  • return compact operational summaries
  • isolate provider interactions behind interfaces
  • make cleanup tasks idempotent

Summary

The notifications.tasks package provides the async execution layer for the notifications system.

It ensures that notification workflows can be processed:

  • safely
  • in batches
  • with retries
  • with provider reconciliation
  • with long-term cleanup support

This makes the notifications system operationally reliable and scalable.