Concurrency
What runs at the same time as what, what ordering you may rely on, and the one rule that explains both.
The rule
A statement's action starts where it is written. The program waits for it at the first read of the binding it produces. Nothing else is promised, and nothing less is.
That single rule is what makes independent statements overlap. Two two-second requests in one feature set cost about two seconds, not four — nobody reads the first result until after the second request has already been started:
(Dashboard: Reporting) {
Request the <sales> from the <sales-url>. (* starts now *)
Request the <traffic> from the <traffic-url>. (* starts now, alongside *)
Compute the <total: sum> from <sales>. (* waits here for <sales> *)
Log <traffic> to the <console>. (* <traffic> is already in *)
Return an <OK: status> with <total>.
}
Effects stay in order
Deferral is an allowlist of value-producing verbs. Anything observable —
Log, Store, Emit, Send,
Publish, Return — never defers: it runs at its own
statement and forces whatever it reads first. So output appears in source
order even though the work behind it did not happen in source order.
Sleep is deliberately excluded from the allowlist, because for
Sleep the delay is the effect.
The levels
| Level | What happens |
|---|---|
| Feature sets | Concurrent. Independent triggers — HTTP requests, events, file changes, sockets — run their feature sets at the same time. |
| Statements | Concurrent where independent, by the rule above. |
| Effects | Ordered. They run at their own statement, in source order. |
| Iterations | Sequential with for each; up to N at a time with parallel for each. |
| Streams | Pipelined. The producer runs ahead of the consumer by a bounded prefetch window. |
parallel for each
The one place a program asks for concurrency explicitly.
(* Sequential: one iteration at a time, deterministic order *)
for each <url> in <urls> {
Request the <page> from <url>.
Store the <page> into the <page-repository>.
}
(* Concurrent: up to N iterations at a time, non-deterministic order *)
parallel for each <url> in <urls> {
Request the <page> from <url>.
Store the <page> into the <page-repository>.
}
(* Explicit bound — match it to what the far end tolerates *)
parallel for each <url> in <urls> with <concurrency: 8> {
Request the <page> from <url>.
}
| Property | Behaviour |
|---|---|
| Completion order | Non-deterministic. Never write code that depends on it. |
| Statements inside one iteration | Sequential, by the rule above |
| Iteration scope | A fresh child scope per iteration; the loop variable and index are bound there |
| Writes to outer bindings | Not possible — bindings are immutable. Accumulate through a repository. |
| Default concurrency | min(item count, max(4, cores × 4)) |
with <concurrency: N> | Overrides the default; N ≤ 0 is rejected and clamped to 1 |
Safe inside a parallel iteration: computing, requesting, storing to repositories, emitting events, logging. Unsafe: anything that assumes an ordering between iterations, or that one iteration observes another's effects. Repository writes from concurrent iterations are serialised by the repository — the result is consistent, the order in which entries land is not defined.
What is shared
Two concurrent executions of the same feature set cannot see each other's variables: each execution gets its own binding scope, and bindings are immutable anyway. The only shared, mutable surfaces are repositories and published symbols.
Application-wide limits
with <concurrency: N> bounds one loop; these bound the whole
application (ARO-0088 §10a). Work queues at a ceiling —
it never fails:
Configure the <application: concurrency> with 8.
Configure the <http-client> with { concurrency: 4, rate: "10/s" }.
Several settings for one category go in one object: two statements naming the same category rebind an immutable binding. A ceiling and a rate are different limits and both apply — one request at a time still exceeds a per-minute quota.
The ceiling counts units of work not already running inside one: every triggered
feature set and every parallel for each iteration. Work started beneath
a slot-holder runs under that slot, which is what makes the ceiling deadlock-free.
The exception is the fire-and-forget event path, where nobody awaits the handler, so
it takes a slot of its own. ARO_CONCURRENCY,
ARO_HTTP_CONCURRENCY and ARO_HTTP_RATE set the same three.
Turning it off, and tuning it
| Variable | Default | Effect |
|---|---|---|
ARO_NO_DEFER | unset | Set it to run every action at its own statement, as the runtime did before deferral. The fastest way to find out whether a suspected bug is order-related. |
ARO_FORCE_WARN_SECONDS | 5 | How long a wait at a force point may take before the runtime warns. 0 disables the warning. |
ARO_STREAM_PREFETCH | 2 | How many elements a stream's producer may run ahead of its consumer. |
Compiled binaries are more conservative
aro build bounds a parallel for each more
tightly than the interpreter does, so statement overlap yields a smaller
— still real — win. Both produce the same results; only throughput differs.
Learn More
The full specification is ARO-0088: Concurrency Model. For what streams do with the same machinery, see Streaming Execution; for request bodies that stream rather than materialise, see Request Bodies.