Attached Executors
Run task bodies in your app container while a separate scheduler holds the database credentials.
Run task bodies in your app container while a separate scheduler holds the database credentials.
An attached executor splits a worker in two. The scheduler — flexiq-server
— holds the database connection, claims jobs, and owns retries, the dead-letter
queue and retention. Your app container runs flexiq executor, which dials the
scheduler, announces the tasks it can run, and executes whatever it is sent.
The app image needs no database credentials and no inbound port.
An in-process worker stays the default and the simplest deployment. Reach for an executor when the app image is large enough that running a second copy of it as a worker is the expensive part — an interpreter or JVM plus model weights resident twice, and a multi-gigabyte image pull on every scale-up.
For the deployment shape — a Compose stack, a Kubernetes sidecar over a shared Unix socket, and the security requirements that go with the attach port — see Deployment.
flexiq-server is configured entirely through the environment — there are no
flags. At minimum it needs a DSN and an attach address:
FLEXIQ_DSN=postgres://user:pass@db/flexiq \
FLEXIQ_LISTEN=0.0.0.0:7777 \
FLEXIQ_ATTACH_TOKEN=$(openssl rand -hex 32) \
flexiq-serverThe scheduler starts lazily, on the first attach. With nothing attached it would claim jobs no executor advertises, and each would fail retryably once the placement timeout elapsed — a retry storm against an idle deployment.
FLEXIQ_ATTACH=scheduler:7777 \
FLEXIQ_ATTACH_TOKEN=... \
flexiq executor --app myapp:queue --slots 4FLEXIQ_ATTACH=scheduler:7777 \
FLEXIQ_ATTACH_TOKEN=... \
flexiq executor ./app.js --slots 4FLEXIQ_ATTACH=scheduler:7777 \
FLEXIQ_ATTACH_TOKEN=... \
java -cp app.jar org.byteveda.flexiq.cli.Cli executor --slots 4Handlers are discovered from the classpath through META-INF/services, so no
application main has to run to register them.
--slots is how many jobs run at once; the scheduler dispatches a task name
only to executors that advertised it, so a handler missing from an executor is a
job that never reaches it.
A secret in argv shows up in ps output and shell history. Every SDK reads
FLEXIQ_ATTACH_TOKEN from the environment and offers no flag for it.
The attach port dispatches code, so the defaults are strict:
FLEXIQ_LISTEN refuses to start without
FLEXIQ_ATTACH_TOKEN.hello must arrive before any other frame; an unauthenticated socket is
dropped rather than queued, and never receives an acknowledgement.unix:/run/flexiq.sock) for a same-pod sidecar; it needs
no token because the filesystem permissions are the boundary.The token is a bearer credential, not transport security. It proves who is attaching; it does not encrypt or integrity-protect the frames, and those frames carry task payloads on a port that dispatches code. On anything but loopback or a Unix socket, terminate mTLS in a proxy in front of the listener and treat the token as the second factor.
flexiq-server does not terminate TLS itself. Setting
FLEXIQ_LISTEN_TLS_CERT or FLEXIQ_LISTEN_TLS_KEY fails at startup rather
than being ignored, so a deployment cannot come up believing it is encrypted
when it is not.
A task body behaves the same as it does on an in-process worker. Progress, log lines and published partials all work — the executor has no storage, so it reports them to the scheduler, which applies them:
@queue.task()
def resize(path: str) -> str:
current_job.update_progress(50)
current_job.log("halfway")
current_job.publish({"stage": "halfway"})
return "done"queue.task("resize", async (path: string) => {
const job = currentJob();
job?.setProgress(50);
job?.log("halfway");
job?.publish({ stage: "halfway" });
return "done";
});@TaskHandler("resize")
public String resize(String path) {
JobContext job = JobContext.current();
job.setProgress(50);
job.log("halfway");
job.publish(Map.of("stage", "halfway"));
return "done";
}Middleware disabled from the dashboard is honoured too. The executor cannot read settings, so the scheduler resolves the list and attaches it to each dispatch — a toggle still takes effect on the next job, with nothing to restart.
Anything that needs the database directly. An enqueue, a workflow submission or a queue inspection raises rather than silently doing nothing, because an enqueue that quietly vanished would be worse than one that failed.
A handful of job fields also arrive as zeros and nulls, because a dispatch frame
carries what running the task needs rather than the whole row: created_at,
scheduled_at, priority, unique_key and notes. metadata is the
exception — middleware reads it, so it rides the frame. A task that needs the
rest wants a worker, not an executor.
The scheduler and its executors upgrade independently. The handshake carries a capability list rather than a version both sides must match, so a newer executor attached to an older scheduler never sends a frame that scheduler could not parse — it degrades instead.
Concretely: a scheduler that does not advertise side_channel gets no progress
or task-log frames, and those calls become no-ops (logged once). Everything else
— dispatch, results, retries, cancellation — is unaffected. The executor logs
which capabilities it negotiated at attach.
Skew in the other direction is covered too. A frame type the receiving side has never heard of is logged once and skipped rather than treated as a corrupt stream: a header declares its own payload length, so the reader stays aligned on the next frame. That holds both ways, so neither an older scheduler nor an older executor drops a live session — and its in-flight jobs — over a frame it did not need.
| Cost | Mitigation |
|---|---|
| CPU-heavy tasks compete with request latency in the app container | Cap --slots; or run a second replica set from the same image with the HTTP server off |
| A network hop per job | Use a Unix socket for same-pod sidecars; attach is a poor fit for microsecond-scale tasks |
| The attach port dispatches code | A Unix socket where the peer is same-pod; off loopback, proxy-terminated mTLS and the token, never the token alone |