> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockx.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime

> The processes and resources that carry a task: Coordinator placement, Worker slot admission, the Python Executor process pool, the IO subsystem and Function Code View, plus the block / bundle clusters and the Sync Invoker.

A task starts at the Client, is placed by a Coordinator, and runs on one Worker; its calls land in that Worker's Python Executor child processes. This page explains every term on that path; implementation details live on the component pages.

## Processes

| Process         | Binary                                                                                           | Responsibility                                                                                                           | State                                                              |
| --------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Coordinator     | `cmd/coordinator`, `cmd/bundle_coordinator`                                                      | Watches Worker heartbeats in etcd, picks a Worker for the Client, and reserves a slot on it                              | No persistent state; multiple instances allowed                    |
| Worker          | `cmd/worker`, `cmd/bundle_worker`                                                                | Source of truth on the execution plane: slot table, the task's three phases, short-term result retention, etcd heartbeat | Process memory; old slots and results are all lost after a restart |
| Python Executor | `python -m blockx_executor`, child processes of the Worker, `EXECUTOR_COUNT` of them (default 8) | Compiles, caches, and executes user functions; suspends on IO                                                            | Caches compiled artifacts by source digest                         |
| Sync Invoker    | `cmd/syncinvoker`                                                                                | Synchronous `Invoke` / `DebugInvoke` / `Precheck`, with its own Executor pool                                            | No slots, no tasks                                                 |
| etcd            | External service                                                                                 | Carries only Worker registration and heartbeats                                                                          | —                                                                  |

## The path of one submission

<Steps>
  <Step title="Placement">
    The Client calls `ReserveWorkerSlot(task_id)` on any Coordinator. The Coordinator picks a candidate from the Worker view it rebuilds from etcd heartbeats, calls `RequestTaskSlot(task_id, ttl_ms)` on it, and hands `worker_addr + slot_id` back to the Client.
  </Step>

  <Step title="Submission">
    The Client connects directly to `worker_addr` and calls `SubmitTask(task, slot_id)`. The Worker validates the slot, creates the task context, pins the code snapshot, and returns `RUNNING`. Without `slot_id`, the Worker requests a slot inline.
  </Step>

  <Step title="Fetching the result">
    The Client calls `WatchTasks([task_id])` on the same Worker to subscribe to the terminal state, or polls with `GetTaskResult(task_id)`. The Coordinator is no longer involved.
  </Step>
</Steps>

blockx-py never connects to an address directly: all RPCs are forwarded through the UDS proxy specified by `PROXY_SOCKET_PATH`. The proxy routes `ReserveWorkerSlot` to the block or bundle Coordinator by gRPC method path, and forwards `WorkerService` requests to the corresponding Worker by the `x-blockx-worker-addr` header. For local development, `examples/local_test_service/local_proxy.py` stands in for this hop.

## Slot

A slot is a task concurrency quota on a Worker:

* Each Worker has a fixed number of slots (`TASK_SLOTS`, default 50); one task occupies one slot.
* `RequestTaskSlot` switches an idle slot to `ALLOCATED` and binds it to `task_id`; if no `SubmitTask` arrives within the TTL (`SlotTTLMs = 5000` on the Coordinator side), the slot is reclaimed automatically.
* Once `SubmitTask` passes validation, the slot enters `RUNNING`; the slot is released the moment the task converges to a terminal state, independent of the result retention window.
* There is no centralized queue. When no slot is available, the Worker returns `NoSlot` (gRPC `ResourceExhausted`) and the caller backs off and retries; blockx-py keeps backing off until it gets a slot.
* The Worker's local slot table is the single source of truth. The Coordinator only holds a discardable derived view; when multiple Coordinators pick the same Worker at the same time, the conflict converges at the Worker's atomic capacity check.

## Inside the Worker

| Part                       | Role                                                                                                                                                                                      | Component page                                            |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| WorkerCore                 | State machine for slots and tasks; pure in-memory, no IO, only decides "may this proceed to the next phase"                                                                               | [Worker](/en/components/worker)                           |
| Call Builder               | Builder phase: expands `functionCallConfig` into a call list                                                                                                                              | [Plugin system](/en/components/plugins)                   |
| Dispatcher + Executor Pool | Delivers calls to Executors in windows; handles suspension, resumption, retries, and sub-calls; at most `TaskMaxInflightCalls = 1024` calls in flight per task                            | [Call execution subsystem](/en/components/call-execution) |
| Function Code View         | Immutable snapshot and epoch of function source; pinned when the task is activated                                                                                                        | [Function Code View](/en/components/function-code)        |
| IO subsystem               | Single entry point for all BlockDB, chain node RPC, Meta, and capability backend access: Worker-level read cache, task-scoped singleflight, IO windows, backend-adaptive admission        | [IO access subsystem](/en/components/io-subsystem)        |
| Backend adapter            | Translates IO requests into real remote calls; the registered backend kinds are `blockdb`, `blockdb_executor`, `rpc`, `meta`, `logicalTypes`, `iceberg`, `localtestservice`, and `router` | [Backend Adapter](/en/components/backend-adapter)         |
| Writer Plugin              | Plugin phase: writes `outputs` back or returns them                                                                                                                                       | [Plugin system](/en/components/plugins)                   |

Between the Executor and the Worker is one long-lived UDS connection: the Worker sends `ExecuteCall`, the Executor replies `CallWaiting` when the user function issues IO or a sub-call, the Worker handles it and sends `ResumeCall`, and finally the Executor ends with `CallCompleted` / `CallFailed`. The Executor never connects to any external service directly.

## Two clusters

The same code starts as two mutually invisible clusters with different `app.Profile` values:

|                          | Block cluster                                                                                | Bundle cluster                                                          |
| ------------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Processes                | `cmd/coordinator` + `cmd/worker`                                                             | `cmd/bundle_coordinator` + `cmd/bundle_worker`                          |
| etcd registration prefix | `/blockx/workers/`                                                                           | `/blockx/bundle-workers/`                                               |
| Coordinator service      | `coordinator.v1.CoordinatorService`                                                          | `bundlecoordinator.v1.BundleCoordinatorService`                         |
| Call Builder             | `BlockTableCallConfig`, `InputsCallConfig`, `BlockBundleCallConfig`                          | `BlockBundleCallConfig`, `InputsCallConfig`                             |
| Writer Plugin            | `BlockTableWriteHandler` (writes per block), `NormalTableWriteHandler`, `ReturnValueHandler` | `BlockTableWriteHandler` (writes per bundle), `NormalTableWriteHandler` |
| stream build             | Off by default                                                                               | On by default                                                           |
| Intended for             | Real-time single-block computation, one-off computation                                      | Large-scale historical backfill                                         |

The task protocol, slot state machine, and heartbeat are identical in both clusters. blockx-py picks the cluster automatically from the config and handler: `BlockTableCallConfig` goes to block, `BlockBundleCallConfig` goes to bundle, anything with `ReturnValueHandler` always goes to block, and `InputsCallConfig` goes to bundle by default.

## Sync Invoker

The Sync Invoker is the second entry point besides tasks: one gRPC unary `Invoke` executes a function once and returns the result, `call_id`, and attributed execution durations on the spot. It reuses the Worker's Executor pool, UDS protocol, Function Code View, and read-only IO subsystem, but does not go through the Coordinator, has no slots, Builders, or Writer Plugins, and rejects all write requests. blockx-py's `function.call(...)` goes through it when running outside an Executor.

## Timing parameters quick reference

| Parameter          | Default                                 | Meaning                                                                                           |
| ------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------- |
| slot TTL           | 5000 ms                                 | Time after which an `ALLOCATED` slot with no submission is reclaimed                              |
| task timeout       | 300000 ms (capped at 8 hours)           | Effective timeout when `task_timeout_ms = 0`, counted from when the Worker accepts the submission |
| Result retention   | 300000 ms                               | Window after the terminal state in which the result can be queried and idempotently hit           |
| Per-call budget    | 5000 ms                                 | CPU + IO backend time, excluding queueing                                                         |
| Heartbeat interval | 200 ms (2000 recommended in production) | How often the Worker writes to etcd                                                               |

See "Timing semantics quick reference" in [Task lifecycle](/en/architecture/task-lifecycle) for the full table.

## Summary

* The Coordinator only does placement and slot reservation; it is stateless and can run as multiple instances. The Worker is the single source of truth on the execution plane.
* A slot is a task concurrency quota on a Worker; there is no centralized queue, so back off and retry when none is available.
* Calls run in the Python Executor child processes on the Worker's own host, and all IO goes back to the Worker's IO subsystem over UDS.
* block and bundle are two clusters built from the same code with different profiles; the Sync Invoker is a synchronous entry point that bypasses the task flow.

Continue reading:

* [Architecture overview](/en/architecture/overview): component diagram and core constraints.
* [Protocols and interfaces](/en/architecture/protocols): gRPC, UDS, and etcd protocol details.
* [Coordinator](/en/components/coordinator) and [Worker](/en/components/worker): how placement, the slot table, and phase admission are implemented.
* [Sync Invoker](/en/components/sync-invoker): implementation of the synchronous invocation entry point.
