python-background-jobs
wshobson/agents
Async task queues and background job patterns for decoupling long-running work from request/response cycles.
What is python-background-jobs?
Implement background job processing using task queues, workers, and event-driven architecture. Use this skill when you need to handle long-running operations, send notifications, process uploads, or integrate with unreliable external services without blocking user requests.
- Enqueue tasks and return job IDs immediately to users
- Manage job state transitions (pending → running → succeeded/failed)
- Configure retry logic with exponential backoff for transient errors
- Design idempotent tasks that safely handle retries and duplicates
- Persist job state for status polling and debugging
- Implement timeout and resource limits on background workers
How to install python-background-jobs
npx skills add https://github.com/wshobson/agents --skill python-background-jobs- Task queue broker (Redis, RabbitMQ, or cloud alternative like AWS SQS)
- Celery or similar task queue library installed
- Database for persisting job state and metadata
How to use python-background-jobs
- 1.Set up a task queue broker (e.g., Redis) and configure Celery with proper timeouts and retry settings
- 2.Create task functions decorated with @app.task, making them idempotent and handling transient vs. permanent errors
- 3.Implement a job repository to persist job state (pending, running, succeeded, failed) with timestamps
- 4.In your API handlers, enqueue tasks and return job IDs immediately instead of waiting for completion
- 5.Expose a GET /jobs/{job_id} endpoint to allow clients to poll job status and retrieve results
- 6.Use idempotency keys when calling external services to ensure safe retries
Use cases
- Sending emails and notifications asynchronously
- Generating reports or exporting large datasets
- Processing file uploads and media transformations
- Charging payments with retry logic and idempotency keys
- Integrating with unreliable external APIs
- Backend engineers building scalable APIs
- Full-stack developers handling async workflows
- DevOps engineers managing task infrastructure
- Teams integrating with external services requiring retries
python-background-jobs FAQ
Task queues decouple work from the request process entirely—tasks run in separate worker processes that can be scaled independently, retried on failure, and monitored separately. Async/await still ties execution to the request lifecycle.
Design tasks to be idempotent: check state before writing, use idempotency keys with external services, or implement upsert patterns. Most queues guarantee at-least-once delivery, so your code must handle duplicates gracefully.
No. Retry only transient errors (network timeouts, temporary service unavailability). Don't retry permanent failures like validation errors, invalid credentials, or declined payments—these will fail repeatedly.
Track queue depth and alert when it grows unexpectedly. Log all state transitions. Expose metrics on job count by status and worker utilization.
The queue will re-deliver the task to another worker (at-least-once delivery). Your task must be idempotent to handle this safely.
Full instructions (SKILL.md)
Source of truth, from wshobson/agents.
name: python-background-jobs description: Python background job patterns including task queues, workers, and event-driven architecture. Use when implementing async task processing, job queues, long-running operations, or decoupling work from request/response cycles.
Python Background Jobs & Task Queues
Decouple long-running or unreliable work from request/response cycles. Return immediately to the user while background workers handle the heavy lifting asynchronously.
When to Use This Skill
- Processing tasks that take longer than a few seconds
- Sending emails, notifications, or webhooks
- Generating reports or exporting data
- Processing uploads or media transformations
- Integrating with unreliable external services
- Building event-driven architectures
Core Concepts
1. Task Queue Pattern
API accepts request, enqueues a job, returns immediately with a job ID. Workers process jobs asynchronously.
2. Idempotency
Tasks may be retried on failure. Design for safe re-execution.
3. Job State Machine
Jobs transition through states: pending → running → succeeded/failed.
4. At-Least-Once Delivery
Most queues guarantee at-least-once delivery. Your code must handle duplicates.
Quick Start
This skill uses Celery for examples, a widely adopted task queue. Alternatives like RQ, Dramatiq, and cloud-native solutions (AWS SQS, GCP Tasks) are equally valid choices.
from celery import Celery
app = Celery("tasks", broker="redis://localhost:6379")
@app.task
def send_email(to: str, subject: str, body: str) -> None:
# This runs in a background worker
email_client.send(to, subject, body)
# In your API handler
send_email.delay("user@example.com", "Welcome!", "Thanks for signing up")
Fundamental Patterns
Pattern 1: Return Job ID Immediately
For operations exceeding a few seconds, return a job ID and process asynchronously.
from uuid import uuid4
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
class JobStatus(Enum):
PENDING = "pending"
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"
@dataclass
class Job:
id: str
status: JobStatus
created_at: datetime
started_at: datetime | None = None
completed_at: datetime | None = None
result: dict | None = None
error: str | None = None
# API endpoint
async def start_export(request: ExportRequest) -> JobResponse:
"""Start export job and return job ID."""
job_id = str(uuid4())
# Persist job record
await jobs_repo.create(Job(
id=job_id,
status=JobStatus.PENDING,
created_at=datetime.utcnow(),
))
# Enqueue task for background processing
await task_queue.enqueue(
"export_data",
job_id=job_id,
params=request.model_dump(),
)
# Return immediately with job ID
return JobResponse(
job_id=job_id,
status="pending",
poll_url=f"/jobs/{job_id}",
)
Pattern 2: Celery Task Configuration
Configure Celery tasks with proper retry and timeout settings.
from celery import Celery
app = Celery("tasks", broker="redis://localhost:6379")
# Global configuration
app.conf.update(
task_time_limit=3600, # Hard limit: 1 hour
task_soft_time_limit=3000, # Soft limit: 50 minutes
task_acks_late=True, # Acknowledge after completion
task_reject_on_worker_lost=True,
worker_prefetch_multiplier=1, # Don't prefetch too many tasks
)
@app.task(
bind=True,
max_retries=3,
default_retry_delay=60,
autoretry_for=(ConnectionError, TimeoutError),
)
def process_payment(self, payment_id: str) -> dict:
"""Process payment with automatic retry on transient errors."""
try:
result = payment_gateway.charge(payment_id)
return {"status": "success", "transaction_id": result.id}
except PaymentDeclinedError as e:
# Don't retry permanent failures
return {"status": "declined", "reason": str(e)}
except TransientError as e:
# Retry with exponential backoff
raise self.retry(exc=e, countdown=2 ** self.request.retries * 60)
Pattern 3: Make Tasks Idempotent
Workers may retry on crash or timeout. Design for safe re-execution.
@app.task(bind=True)
def process_order(self, order_id: str) -> None:
"""Process order idempotently."""
order = orders_repo.get(order_id)
# Already processed? Return early
if order.status == OrderStatus.COMPLETED:
logger.info("Order already processed", order_id=order_id)
return
# Already in progress? Check if we should continue
if order.status == OrderStatus.PROCESSING:
# Use idempotency key to avoid double-charging
pass
# Process with idempotency key
result = payment_provider.charge(
amount=order.total,
idempotency_key=f"order-{order_id}", # Critical!
)
orders_repo.update(order_id, status=OrderStatus.COMPLETED)
Idempotency Strategies:
- Check-before-write: Verify state before action
- Idempotency keys: Use unique tokens with external services
- Upsert patterns:
INSERT ... ON CONFLICT UPDATE - Deduplication window: Track processed IDs for N hours
Pattern 4: Job State Management
Persist job state transitions for visibility and debugging.
class JobRepository:
"""Repository for managing job state."""
async def create(self, job: Job) -> Job:
"""Create new job record."""
await self._db.execute(
"""INSERT INTO jobs (id, status, created_at)
VALUES ($1, $2, $3)""",
job.id, job.status.value, job.created_at,
)
return job
async def update_status(
self,
job_id: str,
status: JobStatus,
**fields,
) -> None:
"""Update job status with timestamp."""
updates = {"status": status.value, **fields}
if status == JobStatus.RUNNING:
updates["started_at"] = datetime.utcnow()
elif status in (JobStatus.SUCCEEDED, JobStatus.FAILED):
updates["completed_at"] = datetime.utcnow()
await self._db.execute(
"UPDATE jobs SET status = $1, ... WHERE id = $2",
updates, job_id,
)
logger.info(
"Job status updated",
job_id=job_id,
status=status.value,
)
Detailed worked examples and patterns
Detailed sections (starting with ## Advanced Patterns) live in references/details.md. Read that file when the navigation summary above is insufficient.
Best Practices Summary
- Return immediately - Don't block requests for long operations
- Persist job state - Enable status polling and debugging
- Make tasks idempotent - Safe to retry on any failure
- Use idempotency keys - For external service calls
- Set timeouts - Both soft and hard limits
- Implement DLQ - Capture permanently failed tasks
- Log transitions - Track job state changes
- Retry appropriately - Exponential backoff for transient errors
- Don't retry permanent failures - Validation errors, invalid credentials
- Monitor queue depth - Alert on backlog growth
Related skills
More from wshobson/agents and the wider catalog.

python-code-style
Modern Python linting, formatting, type checking, and documentation standards with ruff, mypy, and Google-style docstrings.

python-configuration
Externalize Python config via environment variables and typed Pydantic settings with validation.

python-design-patterns
Build maintainable Python code with KISS, Single Responsibility, and composition patterns.

python-error-handling
Python error handling patterns for validation, exceptions, and graceful failure handling.

python-observability
Structured logging, metrics, and distributed tracing patterns for Python applications.

python-packaging
Create and publish distributable Python packages with modern project structure and PyPI integration.