Md Samad
View GitHub
Distributed Systems ZeroMQ Redis FastAPI

Dispatch

A serverless Function-as-a-Service platform built from scratch. Register any Python function, submit it with arguments, and get the result back. The platform handles serialization, scheduling, and distributed execution - exploring three fundamentally different worker communication patterns to understand where their latency and throughput trade-offs actually live.

3
Dispatcher Modes
72
Tests
248
Tasks/sec Peak
System Design

Architecture

Client Python / HTTP HTTP FastAPI Server register / execute / status / result pub/sub Redis task queue + result store notify Dispatcher Local Mode multiprocessing.Pool workers on same machine best for CPU tasks at scale Pull Mode ZMQ REQ/REP workers poll for tasks horizontal scaling Push Mode ZMQ DEALER/ROUTER dispatcher routes actively lowest latency for short tasks Worker (subprocess) deserialize + exec + return result Worker (pull) pull_worker.py - REQ socket Worker (push) push_worker.py - DEALER socket results stored in Redis
End-to-End

How it works

1
Register a Function

The client serializes any Python callable using dill (a pickle extension that handles closures, lambdas, and arbitrary callables) and POSTs it to /register_function. The FastAPI server stores the serialized payload in Redis under a UUID and returns a function_id. Any Python function can be registered - no pre-registration or interface contract required.

2
Submit a Task

The client POSTs to /execute_function with the function_id and a dill-serialized (args, kwargs) tuple. The server creates a task record in Redis with status QUEUED, pushes the task ID to a Redis list, and publishes a notification on a pub/sub channel. The response returns a task_id immediately - the server never blocks waiting for execution.

3
Dispatcher Picks Up the Task

The dispatcher subscribes to the Redis pub/sub channel and wakes up when a new task arrives. It pops the task from the Redis queue and routes it to a free worker - the routing strategy differs per mode. In local mode, it submits to a multiprocessing.Pool. In pull mode, it waits for a worker to REQ and sends the task over ZMQ REP. In push mode, it proactively routes to the next available DEALER socket using a ROUTER, eliminating the round-trip poll.

4
Worker Executes and Returns

The worker deserializes the function and arguments, sets the task status to RUNNING in Redis, and calls the function. On success it serializes the return value with dill and stores it in Redis with status COMPLETED. On exception it serializes the exception object and stores it with status FAILED. Failures are surfaced as structured data, not silently dropped.

5
Client Polls for Result

The client polls /status/{task_id} until the status transitions to COMPLETED or FAILED, then fetches the result from /result/{task_id}. The result is deserialized with dill on the client side - the round-trip preserves the full Python type, not just a JSON-serializable subset. An exception from the worker arrives as a deserializable Python exception object.

6
Benchmarking the Three Modes

All three modes were benchmarked under weak scaling (10 tasks per worker, 1-8 workers) using two workloads: noop tasks (pure overhead) and 100ms sleep tasks (parallelism test). Push mode dominates noop throughput (248 tasks/sec at 2 workers) because the dispatcher immediately routes to idle workers without waiting for a poll. Local mode dominates sleep-task throughput at 8 workers (1.08s total vs. 2.1s for ZMQ modes) because multiprocessing.Pool achieves true OS-level parallelism with no network overhead.

Stack

Technologies

API Server
FastAPI Uvicorn Pydantic Python 3.12
Messaging / Queue
Redis ZeroMQ (PyZMQ) REQ/REP DEALER/ROUTER
Serialization
dill base64 multiprocessing.Pool
Testing
pytest unittest.mock conftest.py
Benchmarking
concurrent.futures requests weak scaling
Packaging
pyproject.toml pip install -e .