RabbitMQ
Job Splitting
Task Handling
Message Queuing
Task Results

RabbitMQ how to split jobs to tasks and handle results

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Large jobs are easier to scale when split into independent tasks and processed by multiple workers through RabbitMQ. The challenging part is not publishing tasks, but handling retries, partial failure, and deterministic final aggregation. A robust design uses clear task boundaries, correlation metadata, idempotent workers, and explicit completion rules.

Define Job and Task Boundaries First

Before writing queue code, define the unit of work precisely. A task should run independently and be safe to retry without producing duplicate side effects. Every task message should contain job_id, task_id, and enough payload to execute without shared mutable state.

Good boundaries keep workers stateless and horizontally scalable. Poor boundaries force workers to coordinate through shared memory or synchronous calls, which removes most of the benefit of queue-based processing.

Publish Tasks with Traceable Metadata

Use one exchange for task routing and persist expected task count in your job store. The aggregator needs this value to know when to close a job.

python
1import json
2import pika
3import uuid
4
5conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
6ch = conn.channel()
7
8ch.exchange_declare(exchange='jobs.x', exchange_type='direct', durable=True)
9ch.queue_declare(queue='tasks.q', durable=True)
10ch.queue_bind(exchange='jobs.x', queue='tasks.q', routing_key='task')
11
12job_id = str(uuid.uuid4())
13chunks = ['part-a', 'part-b', 'part-c']
14
15for i, chunk in enumerate(chunks, start=1):
16    payload = json.dumps({'job_id': job_id, 'task_id': i, 'payload': chunk})
17    ch.basic_publish(
18        exchange='jobs.x',
19        routing_key='task',
20        body=payload,
21        properties=pika.BasicProperties(
22            delivery_mode=2,
23            correlation_id=job_id,
24            message_id=f'{job_id}:{i}'
25        )
26    )
27
28conn.close()

Durable queues and persistent messages improve recovery during broker restarts.

Build Idempotent Workers and Safe Acknowledgement Flow

Workers should ack only after task processing and result publishing both succeed. If either step fails, the message should be retried or dead-lettered according to policy.

python
1def process_task(ch, method, properties, body):
2    msg = json.loads(body)
3
4    # idempotency guard in your store should reject duplicate task_id per job_id
5    result = run_unit_of_work(msg['payload'])
6
7    out = json.dumps({
8        'job_id': msg['job_id'],
9        'task_id': msg['task_id'],
10        'result': result
11    })
12
13    ch.basic_publish(exchange='results.x', routing_key='result', body=out)
14    ch.basic_ack(delivery_tag=method.delivery_tag)

If run_unit_of_work writes to external systems, idempotency keys are essential because redelivery is always possible.

Aggregate Results with Explicit Completion Criteria

A dedicated aggregator consumes result messages and updates job state in a database. Track expected_tasks, completed_tasks, and failed_tasks. Mark the job complete only when completed_tasks == expected_tasks.

Do not use queue emptiness as completion signal. Queues may be temporarily empty while workers are still processing or retrying tasks.

A practical aggregator can also store per-task timestamps and error details. This makes root-cause analysis far easier when one task class slows down or fails repeatedly.

Add Bounded Retries and Dead Letter Queues

Unlimited retries create noisy failure loops and hidden load. Configure dead letter routing and enforce small retry ceilings.

python
1ch.exchange_declare(exchange='dlx.x', exchange_type='direct', durable=True)
2ch.queue_declare(
3    queue='tasks.q',
4    durable=True,
5    arguments={
6        'x-dead-letter-exchange': 'dlx.x',
7        'x-dead-letter-routing-key': 'task.failed'
8    }
9)
10ch.queue_declare(queue='tasks.dlq', durable=True)
11ch.queue_bind(exchange='dlx.x', queue='tasks.dlq', routing_key='task.failed')

When retrying, increment a retry header and stop after a fixed maximum. Alert on dead letter queue growth by routing key.

Prefer Async Completion APIs Over Request-Reply Blocking

RabbitMQ supports request-reply, but batch workflows usually perform better with asynchronous completion. Return job_id to clients and expose a status endpoint instead of holding long HTTP requests.

A typical pattern:

  1. client submits job and receives 202 Accepted plus job_id
  2. client polls GET /jobs/<id>
  3. aggregator writes terminal state and result location

This model decouples web timeout constraints from queue processing duration.

Common Pitfalls

  • Creating tasks that still depend on shared global state.
  • Publishing messages without job_id and task_id metadata.
  • Acknowledging task messages before result persistence.
  • Treating queue empty state as job completion.
  • Running unlimited retries without dead letter controls.
  • Skipping idempotency checks and producing duplicate side effects.

Summary

  • Define strict task boundaries and include full execution context per message.
  • Publish persistent task messages with correlation metadata.
  • Keep worker execution idempotent and acknowledge only after successful completion.
  • Aggregate using expected versus completed counters, not queue emptiness.
  • Use bounded retries and dead letter queues for controlled failure handling.
  • Prefer asynchronous job status APIs for scalable client interaction.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.