Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/wshobson-python-background-jobs/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-13 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 66% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 56% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 97% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 195% | 0% |
Decouple long-running or unreliable work from request/response cycles. Return immediately to the user while background workers handle the heavy lifting asynchronously.
API accepts request, enqueues a job, returns immediately with a job ID. Workers process jobs asynchronously.
Tasks may be retried on failure. Design for safe re-execution.
Jobs transition through states: pending → running → succeeded/failed.
Most queues guarantee at-least-once delivery. Your code must handle duplicates.
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.
pythonfrom 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")
For operations exceeding a few seconds, return a job ID and process asynchronously.
pythonfrom 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}", )
Configure Celery tasks with proper retry and timeout settings.
pythonfrom 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)
Workers may retry on crash or timeout. Design for safe re-execution.
python@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:
INSERT ... ON CONFLICT UPDATEPersist job state transitions for visibility and debugging.
pythonclass 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 sections (starting with ## Advanced Patterns) live in references/details.md. Read that file when the navigation summary above is insufficient.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 10,660 | 8,758 | -18% | 1 | 1 | 0% | 2,509 | 3,902 | +56% | 0 | 0 | — |
case-02 | pass→pass | 9,141 | 6,885 | -25% | 1 | 1 | 0% | 1,674 | 3,305 | +97% | 0 | 0 | — |
case-03 | pass→pass | 5,427 | 5,663 | +4% | 1 | 1 | 0% | 1,041 | 3,073 | +195% | 0 | 0 | — |
case-04 | pass→pass | 5,977 | 10,055 | +68% | 1 | 1 | 0% | 1,461 | 3,977 | +172% | 0 | 0 | — |
case-05 | pass→pass | 5,259 | 5,105 | -3% | 1 | 1 | 0% | 1,075 | 2,937 | +173% | 0 | 0 | — |
case-06 | pass→pass | 8,558 | 6,875 | -20% | 1 | 1 | 0% | 2,073 | 3,530 | +70% | 0 | 0 | — |
case-07 | pass→pass | 4,935 | 4,951 | +0% | 1 | 1 | 0% | 1,104 | 2,880 | +161% | 0 | 0 | — |
case-08 | pass→pass | 14,567 | 9,545 | -34% | 1 | 1 | 0% | 2,721 | 4,071 | +50% | 0 | 0 | — |
case-09 | pass→pass | 15,036 | 16,278 | +8% | 1 | 1 | 0% | 3,312 | 4,686 | +41% | 0 | 0 | — |
case-10 | pass→pass | 15,068 | 11,888 | -21% | 1 | 1 | 0% | 2,807 | 4,378 | +56% | 0 | 0 | — |
case-11 | pass→pass | 66,370 | 4,837 | -93% | 1 | 1 | 0% | 1,085 | 3,017 | +178% | 0 | 0 | — |
case-12 | pass→pass | 4,871 | 1,674 | -66% | 1 | 1 | 0% | 966 | 2,178 | +125% | 0 | 0 | — |
case-17 | pass→pass | 3,545 | 2,847 | -20% | 1 | 1 | 0% | 641 | 2,368 | +269% | 0 | 0 | — |
case-13 | fail→pass | 5,239 | 3,627 | -31% | 1 | 1 | 0% | 1,201 | 2,605 | +117% | 0 | 0 | — |
case-14 | pass→pass | 7,851 | 5,768 | -27% | 1 | 1 | 0% | 1,710 | 3,064 | +79% | 0 | 0 | — |
case-15 | pass→pass | 2,747 | 3,305 | +20% | 1 | 1 | 0% | 568 | 2,470 | +335% | 0 | 0 | — |
case-16 | pass→pass | 6,626 | 5,040 | -24% | 1 | 1 | 0% | 1,302 | 2,919 | +124% | 0 | 0 | — |
case-18 | pass→pass | 14,152 | 15,936 | +13% | 1 | 1 | 0% | 2,693 | 4,826 | +79% | 0 | 0 | — |
case-19 | pass→pass | 11,487 | 8,955 | -22% | 1 | 1 | 0% | 2,448 | 3,524 | +44% | 0 | 0 | — |
case-20 | fail→pass | 11,105 | 7,732 | -30% | 1 | 1 | 0% | 1,944 | 3,222 | +66% | 0 | 0 | — |
case-21 | pass→pass | 7,028 | 6,450 | -8% | 1 | 1 | 0% | 1,327 | 3,244 | +144% | 0 | 0 | — |
case-22 | pass→pass | 2,932 | 4,060 | +38% | 1 | 1 | 0% | 606 | 2,575 | +325% | 0 | 0 | — |
case-23 | pass→pass | 4,939 | 4,118 | -17% | 1 | 1 | 0% | 926 | 2,554 | +176% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.