Run a script many times, compile it once.
Quickstart
julia --project=temp -e 'using Pkg; Pkg.add(url="https://github.com/tecosaur/DaemonicCabal.jl.git"); using DaemonicCabal; DaemonicCabal.install()'Once installed, DaemonicCabal.jl provides juliaclient, which serves as a
drop-in replacement for a subset of julia, with a few extra tricks.
~$ juliaclient --help
juliaclient [switches] -- [programfile] [args...]
Switches (a '*' marks the default value, if applicable):
-v, --version Display version information
-h, --help Print this message
-P, --project[=<dir>|@.] Set <dir> as the home project/environment
-e, --eval <expr> Evaluate <expr>
-E, --print <expr> Evaluate <expr> and display the result
-L, --load <file> Load <file> immediately on all processors
-i Interactive mode; REPL runs and `isinteractive()` is true
-t, --threads <N|auto>[,<M|auto>] Launch N threads (and M interactive threads)
-q, --quiet Quiet startup: no banner, suppress REPL warnings
--banner={yes|no|auto*} Enable or disable startup banner
--color={yes|no|auto*} Enable or disable color text
--history-file={yes*|no} Load or save history
Client-specific switches:
-a, --address <addr> Connect to conductor at <addr> instead of default
--session[=<label>] Reuse worker state in Main module. With a label,
multiple clients can share the same session.
--sync Attach to shared REPL (requires --session=<label>)
--revise[=yes|no*] Enable or disable Revise.jl integration
--restart Kill workers for the project and exit
--sandbox Run in an isolated sandbox (Linux only)
--status[=live|json] Show the state of the workers, optionally in json
Daemon management (systemd):
systemctl --user {start | stop | restart | status} julia-daemon
Supported platforms: Linux, macOS, FreeBSD, and OpenBSD.
By default, each juliaclient invocation runs in a fresh module. Use
--session to persist state in Main across invocations:
~$ juliaclient --session -e 'x = 42' ~$ juliaclient --session -e 'println(x)' # prints 42
Without a label, session affinity is based on the parent process (shell). With a label, multiple clients share the same session:
# Terminal A ~$ juliaclient --session=shared -e 'data = [1,2,3]' # Terminal B (different shell, same label) ~$ juliaclient --session=shared -e 'push!(data, 4); data' # [1, 2, 3, 4]
Labeled sessions bypass the per-worker client limit, and workers holding idle
labelled sessions persist for longer, their minimum lifetime is the geometric
mean of MIN_TTL and MAX_TTL.
Adding --sync upgrades a labeled session to a shared terminal — multiple
terminals attach to the same live REPL.
# Terminal A: start a shared REPL ~$ juliaclient --sync --session=work # Terminal B: attach to the same REPL ~$ juliaclient --sync --session=work # Terminal C: inject code (output appears in both REPLs) ~$ juliaclient --sync --session=work -e 'x = 42'
Interactive clients share a merged stdin and broadcast stdout/stderr.
Non-interactive clients (-e, -E, programfile) run in the shared Main with
output broadcast to all attached terminals. Ctrl-D detaches without killing the
session; the REPL exits when the last interactive client disconnects.
Sandboxed workers run inside Linux user namespaces (no root required) with a minimal root filesystem, providing isolation without containers.
Pass --sandbox to run in an isolated environment with write access only to the
working directory:
~$ cd ~/myproject
~$ juliaclient --sandbox -e 'println(pwd()); touch("ok")' # works
~$ juliaclient --sandbox -e 'rm("/etc/passwd")' # permission denied
The sandbox gives read-write access to:
- The project directory when the working directory is inside it (the project mount subsumes the working directory)
- Just the working directory otherwise (the project is mounted read-only)
Named environments (@v1.11 etc.) are not mounted read-write.
When listening on TCP (JULIA_DAEMON_SERVER=tcp://...), non-loopback connections
are automatically sandboxed. Each remote client gets a fresh, isolated worker
with a read-only view of the host depot and no access to the host home directory.
Optional cgroup limits can restrict memory and CPU per sandbox.
Local (loopback) TCP clients are not sandboxed unless --sandbox is passed.
Remote clients on non-Linux platforms are rejected.
Environment variables read at daemon startup:
| Variable | Default | Description |
|---|---|---|
JULIA_DAEMON_RUNTIME | (platform-specific) | Runtime directory for sockets and PID file |
JULIA_DAEMON_SERVER | <runtime_dir>/conductor.sock | Socket path or tcp://host:port for TCP mode |
JULIA_DAEMON_BIND | (from server addr) | TCP bind address override (e.g. 0.0.0.0) |
JULIA_DAEMON_PORTS | unset | Worker port range in TCP mode (e.g. 10000-10100) |
JULIA_DAEMON_WORKER_EXECUTABLE | julia | Julia binary for workers |
JULIA_DAEMON_WORKER_ARGS | --startup-file=no | Arguments passed to worker processes (split on whitespace) |
JULIA_DAEMON_WORKER_PROJECT | (required) | Project for the worker itself (not client projects) |
JULIA_DAEMON_WORKER_MAXCLIENTS | 1 | Max clients per worker (0 = unlimited) |
JULIA_DAEMON_MAX_TTL | 7200 | Idle worker deadline in seconds; always culled past this (2h). Supersedes JULIA_DAEMON_WORKER_TTL, falling back to it when unset |
JULIA_DAEMON_MIN_TTL | 120 | Protected floor: idle workers younger than this are never evicted under memory pressure |
JULIA_DAEMON_LABEL_TTL | 90 | Session label retention after last client disconnects (seconds) |
JULIA_DAEMON_PING_INTERVAL | 30 | Health check interval (seconds) |
JULIA_DAEMON_PING_TIMEOUT | 5 | Ping timeout before killing unresponsive worker (seconds) |
JULIA_DAEMON_REVISE | no | Enable Revise.jl by default (also settable with --revise) |
Memory-pressure eviction:
| Variable | Default | Description |
|---|---|---|
JULIA_DAEMON_MEMORY_PRESSURE | 1 | Shed idle workers under host memory pressure (0 to disable) |
JULIA_DAEMON_PSI_THRESHOLD | 10.0 | PSI some avg10 % that counts as pressure (when PSI is the active source) |
JULIA_DAEMON_MEMFREE_LOW | 10% | Enter pressure below this free memory; % of total or absolute (2G) |
JULIA_DAEMON_MEMFREE_HIGH | 15% | Leave pressure above this free memory (hysteresis; must exceed MEMFREE_LOW) |
Sandbox configuration (Linux only, applies to both --sandbox and remote clients):
| Variable | Default | Description |
|---|---|---|
JULIA_DAEMON_SANDBOX_REMOTE_CLIENTS | 1 | Sandbox remote (non-loopback) TCP clients (0 to disable) |
JULIA_DAEMON_SANDBOX_EMPTY_ENVIRONMENT | 1 | Mask ~/.julia/environments in sandbox (0 to expose host envs) |
JULIA_DAEMON_SANDBOX_MAX_MEMORY | unset | Per-sandbox cgroup memory limit (e.g. 4G, 512M) |
JULIA_DAEMON_SANDBOX_MAX_CPU | unset | Per-sandbox cgroup CPU limit as percentage (e.g. 200 = 2 cores) |
JULIA_DAEMON_SANDBOX_SESSION_BYPASS | 0 | Allow remote --session=<label> to join local unsandboxed workers |
Runtime directory defaults: Linux /run/user/$UID/julia-daemon, macOS
~/Library/Application Support/julia-daemon, BSD /tmp/julia-daemon-$UID.
The client reads JULIA_DAEMON_SERVER and JULIA_DAEMON_RUNTIME to locate the
conductor socket.
Idle workers are large (a warmed Julia process is hundreds of MB), so on a busy
or shared host the conductor treats them as a memory-pressure-reactive cache:
when the host runs low on memory it sheds the least valuable idle workers,
leaving them alone while RAM is plentiful. This is on by default; set
JULIA_DAEMON_MEMORY_PRESSURE=0 for TTL-only behaviour.
A worker is eligible for eviction only once it has been idle past MIN_TTL (a
recently-used worker is never culled), and the conductor evicts the
lowest-value-per-byte first — a worker kept warm by frequent or long-running
use ranks above a cold one, and a large worker yields before a small one of
equal warmth. Eviction never touches a worker with a connected client. An
evicted worker simply cold-starts on its next invocation, exactly as it would
after its idle MAX_TTL deadline.
Pressure is read from the kernel’s PSI stall information where available, else
from the free-memory level (MemAvailable). On FreeBSD only the pressure trigger
is implemented (workers are ranked without a size term); OpenBSD is TTL-only.
juliaclient --status shows each idle worker’s eviction warmth while pressure
handling is active.
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ Client (Zig) │─────▶│ Conductor (Zig) │─────▶│ Worker Pool (Julia) │
└────────┬────────┘ └──────────────────┘ └────────┬─────────────┘
│ │
└────────────────── stdio ─────────────────────────┘
- Client: Zig binary users invoke. Connects to the conductor, then proxies stdio/signals directly with the assigned worker.
- Conductor: Zig daemon managing the worker pool. Spawns workers per-project and routes client requests.
- Worker: Julia process serving client sessions for a single project.
Platform-specific I/O: io_uring on Linux, kqueue on macOS/BSD.
- Workers are per-project, so different projects incur separate startup costs.