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.
Architecture
How it works
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.
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.
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.
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.
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.
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.