← Back to Documentation

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

LevelWhat happens
Feature setsConcurrent. Independent triggers — HTTP requests, events, file changes, sockets — run their feature sets at the same time.
StatementsConcurrent where independent, by the rule above.
EffectsOrdered. They run at their own statement, in source order.
IterationsSequential with for each; up to N at a time with parallel for each.
StreamsPipelined. 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>.
}
PropertyBehaviour
Completion orderNon-deterministic. Never write code that depends on it.
Statements inside one iterationSequential, by the rule above
Iteration scopeA fresh child scope per iteration; the loop variable and index are bound there
Writes to outer bindingsNot possible — bindings are immutable. Accumulate through a repository.
Default concurrencymin(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

VariableDefaultEffect
ARO_NO_DEFERunsetSet 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_SECONDS5How long a wait at a force point may take before the runtime warns. 0 disables the warning.
ARO_STREAM_PREFETCH2How 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.