Python background job patterns including task queues
Python Background Jobs & Task Queues

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.
--- 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
Overview
Decouple long-running or unreliable work from request/response cycles. Return immediately to the user while background workers handle the heavy lifting asynchronously.
Manual install (npx, ZIP)
npx skills add wshobson/agents --skill python-background-jobs -g -a claude-codemkdir -p ~/.claude/skills && curl -sL https://codeload.github.com/wshobson/agents/tar.gz/4236bb91f839 \
| tar -xz -C ~/.claude/skills --strip-components=4 agents-4236bb91f839/plugins/python-development/skills/python-background-jobsNew-Item -ItemType Directory -Force "$HOME\.claude\skills" | Out-Null; Invoke-WebRequest "https://m.agentalley.io/zip/8ef01bd551ccdfe837d7/python-background-jobs.zip" -OutFile "$env:TEMP\python-background-jobs.zip"; Expand-Archive "$env:TEMP\python-background-jobs.zip" -DestinationPath "$HOME\.claude\skills" -ForceConnects with Agent Alley MCP · agentalley.io/mcp
Files included
SKILL.md7.1 KBreferences/1 file · 3.3 KB
Read the files here
Show the full text242 lines
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("[email protected]", "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
| 1 | |
| 2 | name python-background-jobs |
| 3 | 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. |
| 4 | |
| 5 | |
| 6 | # Python Background Jobs & Task Queues |
| 7 | |
| 8 | Decouple long-running or unreliable work from request/response cycles. Return immediately to the user while background workers handle the heavy lifting asynchronously. |
| 9 | |
| 10 | ## When to Use This Skill |
| 11 | |
| 12 | Processing tasks that take longer than a few seconds |
| 13 | Sending emails, notifications, or webhooks |
| 14 | Generating reports or exporting data |
| 15 | Processing uploads or media transformations |
| 16 | Integrating with unreliable external services |
| 17 | Building event-driven architectures |
| 18 | |
| 19 | ## Core Concepts |
| 20 | |
| 21 | ### 1. Task Queue Pattern |
| 22 | |
| 23 | API accepts request, enqueues a job, returns immediately with a job ID. Workers process jobs asynchronously. |
| 24 | |
| 25 | ### 2. Idempotency |
| 26 | |
| 27 | Tasks may be retried on failure. Design for safe re-execution. |
| 28 | |
| 29 | ### 3. Job State Machine |
| 30 | |
| 31 | Jobs transition through states: pending → running → succeeded/failed. |
| 32 | |
| 33 | ### 4. At-Least-Once Delivery |
| 34 | |
| 35 | Most queues guarantee at-least-once delivery. Your code must handle duplicates. |
| 36 | |
| 37 | ## Quick Start |
| 38 | |
| 39 | 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. |
| 40 | |
| 41 | |
| 42 | from celery import Celery |
| 43 | |
| 44 | app = Celery("tasks", broker="redis://localhost:6379") |
| 45 | |
| 46 | @app.task |
| 47 | def send_email(to: str, subject: str, body: str) -> None: |
| 48 | # This runs in a background worker |
| 49 | email_client.send(to, subject, body) |
| 50 | |
| 51 | # In your API handler |
| 52 | send_email.delay("[email protected]", "Welcome!", "Thanks for signing up") |
| 53 | |
| 54 | |
| 55 | ## Fundamental Patterns |
| 56 | |
| 57 | ### Pattern 1: Return Job ID Immediately |
| 58 | |
| 59 | For operations exceeding a few seconds, return a job ID and process asynchronously. |
| 60 | |
| 61 | |
| 62 | from uuid import uuid4 |
| 63 | from dataclasses import dataclass |
| 64 | from enum import Enum |
| 65 | from datetime import datetime |
| 66 | |
| 67 | class JobStatus(Enum): |
| 68 | PENDING = "pending" |
| 69 | RUNNING = "running" |
| 70 | SUCCEEDED = "succeeded" |
| 71 | FAILED = "failed" |
| 72 | |
| 73 | @dataclass |
| 74 | class Job: |
| 75 | id: str |
| 76 | status: JobStatus |
| 77 | created_at: datetime |
| 78 | started_at: datetime | None = None |
| 79 | completed_at: datetime | None = None |
| 80 | result: dict | None = None |
| 81 | error: str | None = None |
| 82 | |
| 83 | # API endpoint |
| 84 | async def start_export(request: ExportRequest) -> JobResponse: |
| 85 | """Start export job and return job ID.""" |
| 86 | job_id = str(uuid4()) |
| 87 | |
| 88 | # Persist job record |
| 89 | await jobs_repo.create(Job( |
| 90 | id=job_id, |
| 91 | status=JobStatus.PENDING, |
| 92 | created_at=datetime.utcnow(), |
| 93 | )) |
| 94 | |
| 95 | # Enqueue task for background processing |
| 96 | await task_queue.enqueue( |
| 97 | "export_data", |
| 98 | job_id=job_id, |
| 99 | params=request.model_dump(), |
| 100 | ) |
| 101 | |
| 102 | # Return immediately with job ID |
| 103 | return JobResponse( |
| 104 | job_id=job_id, |
| 105 | status="pending", |
| 106 | poll_url=f"/jobs/{job_id}", |
| 107 | ) |
| 108 | |
| 109 | |
| 110 | ### Pattern 2: Celery Task Configuration |
| 111 | |
| 112 | Configure Celery tasks with proper retry and timeout settings. |
| 113 | |
| 114 | |
| 115 | from celery import Celery |
| 116 | |
| 117 | app = Celery("tasks", broker="redis://localhost:6379") |
| 118 | |
| 119 | # Global configuration |
| 120 | app.conf.update( |
| 121 | task_time_limit=3600, # Hard limit: 1 hour |
| 122 | task_soft_time_limit=3000, # Soft limit: 50 minutes |
| 123 | task_acks_late=True, # Acknowledge after completion |
| 124 | task_reject_on_worker_lost=True, |
| 125 | worker_prefetch_multiplier=1, # Don't prefetch too many tasks |
| 126 | ) |
| 127 | |
| 128 | @app.task( |
| 129 | bind=True, |
| 130 | max_retries=3, |
| 131 | default_retry_delay=60, |
| 132 | autoretry_for=(ConnectionError, TimeoutError), |
| 133 | ) |
| 134 | def process_payment(self, payment_id: str) -> dict: |
| 135 | """Process payment with automatic retry on transient errors.""" |
| 136 | try: |
| 137 | result = payment_gateway.charge(payment_id) |
| 138 | return {"status": "success", "transaction_id": result.id} |
| 139 | except PaymentDeclinedError as e: |
| 140 | # Don't retry permanent failures |
| 141 | return {"status": "declined", "reason": str(e)} |
| 142 | except TransientError as e: |
| 143 | # Retry with exponential backoff |
| 144 | raise self.retry(exc=e, countdown=2 ** self.request.retries * 60) |
| 145 | |
| 146 | |
| 147 | ### Pattern 3: Make Tasks Idempotent |
| 148 | |
| 149 | Workers may retry on crash or timeout. Design for safe re-execution. |
| 150 | |
| 151 | |
| 152 | @app.task(bind=True) |
| 153 | def process_order(self, order_id: str) -> None: |
| 154 | """Process order idempotently.""" |
| 155 | order = orders_repo.get(order_id) |
| 156 | |
| 157 | # Already processed? Return early |
| 158 | if order.status == OrderStatus.COMPLETED: |
| 159 | logger.info("Order already processed", order_id=order_id) |
| 160 | return |
| 161 | |
| 162 | # Already in progress? Check if we should continue |
| 163 | if order.status == OrderStatus.PROCESSING: |
| 164 | # Use idempotency key to avoid double-charging |
| 165 | pass |
| 166 | |
| 167 | # Process with idempotency key |
| 168 | result = payment_provider.charge( |
| 169 | amount=order.total, |
| 170 | idempotency_key=f"order-{order_id}", # Critical! |
| 171 | ) |
| 172 | |
| 173 | orders_repo.update(order_id, status=OrderStatus.COMPLETED) |
| 174 | |
| 175 | |
| 176 | **Idempotency Strategies:** |
| 177 | |
| 178 | **Check-before-write**: Verify state before action |
| 179 | **Idempotency keys**: Use unique tokens with external services |
| 180 | **Upsert patterns**: `INSERT ... ON CONFLICT UPDATE` |
| 181 | **Deduplication window**: Track processed IDs for N hours |
| 182 | |
| 183 | ### Pattern 4: Job State Management |
| 184 | |
| 185 | Persist job state transitions for visibility and debugging. |
| 186 | |
| 187 | |
| 188 | class JobRepository: |
| 189 | """Repository for managing job state.""" |
| 190 | |
| 191 | async def create(self, job: Job) -> Job: |
| 192 | """Create new job record.""" |
| 193 | await self._db.execute( |
| 194 | """INSERT INTO jobs (id, status, created_at) |
| 195 | VALUES ($1, $2, $3)""", |
| 196 | job.id, job.status.value, job.created_at, |
| 197 | ) |
| 198 | return job |
| 199 | |
| 200 | async def update_status( |
| 201 | self, |
| 202 | job_id: str, |
| 203 | status: JobStatus, |
| 204 | **fields, |
| 205 | ) -> None: |
| 206 | """Update job status with timestamp.""" |
| 207 | updates = {"status": status.value, **fields} |
| 208 | |
| 209 | if status == JobStatus.RUNNING: |
| 210 | updates["started_at"] = datetime.utcnow() |
| 211 | elif status in (JobStatus.SUCCEEDED, JobStatus.FAILED): |
| 212 | updates["completed_at"] = datetime.utcnow() |
| 213 | |
| 214 | await self._db.execute( |
| 215 | "UPDATE jobs SET status = $1, ... WHERE id = $2", |
| 216 | updates, job_id, |
| 217 | ) |
| 218 | |
| 219 | logger.info( |
| 220 | "Job status updated", |
| 221 | job_id=job_id, |
| 222 | status=status.value, |
| 223 | ) |
| 224 | |
| 225 | |
| 226 | ## Detailed worked examples and patterns |
| 227 | |
| 228 | Detailed sections (starting with `## Advanced Patterns`) live in `references/details.md`. Read that file when the navigation summary above is insufficient. |
| 229 | |
| 230 | ## Best Practices Summary |
| 231 | |
| 232 | **Return immediately** - Don't block requests for long operations |
| 233 | **Persist job state** - Enable status polling and debugging |
| 234 | **Make tasks idempotent** - Safe to retry on any failure |
| 235 | **Use idempotency keys** - For external service calls |
| 236 | **Set timeouts** - Both soft and hard limits |
| 237 | **Implement DLQ** - Capture permanently failed tasks |
| 238 | **Log transitions** - Track job state changes |
| 239 | **Retry appropriately** - Exponential backoff for transient errors |
| 240 | **Don't retry permanent failures** - Validation errors, invalid credentials |
| 241 | **Monitor queue depth** - Alert on backlog growth |
| 242 |
Details
- Updated
- Sep 21, 2026
- License
- MIT
- Stars on the repo
- ★ 39,857
- Checked
- Source
- wshobson/agents ↗
- Creator
- wshobson · 179 listings
Similar skills
See all →
SkillA task is too large for one model passAdvisor orchestrator worker
SkillRun the CCS Align seat's hourly breathing cycleCCS Align — Worker Watch seat (Phases 0–2 shipped; Phase 3 = verify / sign-off)
SkillSummarize and review what changed while you were awayCatchup — Async-Team Return Briefing
SkillMaster Python asyncio, concurrent programmingAsync python patterns
SkillMaster Go concurrency with goroutines, channelsGo concurrency patterns
SkillDesign microservices architectures with service boundariesMicroservices patternsMore from wshobson
SkillMaster C/.NET backend development patterns for building
SkillImplement WCAG 2.2 compliant interfaces with mobileAccessibility compliance
SkillMaster Material Design 3 and Jetpack Compose patternsAndroid Mobile Design
SkillMigrate from AngularJS to Angular using hybrid modeAngular migration
SkillUnderstand anti-reversing, obfuscationAnti reversing techniques
SkillBuild production Apache Airflow DAGs with best practicesApache Airflow DAG Patterns