Runtime tuning controls how SoAI admits work, how much of it runs at once, how long an attempt may take, and when idle model memory is released. This page holds those controls and their factory values, while Resilience and self-healing describes what the scheduler does with them under failure and Capacity planning covers choosing values for a measured workload. Raising a limit moves pressure somewhere else, usually to memory, a provider quota, or a backend that was already saturated, so change one control at a time and read completed throughput rather than queue depth.
Change a routing setting in the SoAI WebUI
Opening Advanced settings requires an Administrator with Configuration changes
(CONFIG_PATCH).
- Select the Settings button at the right of the header, then Settings.
- Select Advanced in the page header.
- Paste the full configuration path into Search settings..., change the value, and select Save.
- Select Restart SoAI if the WebUI reports a pending restart.
MODELS.ROUTING is one of the three sections
SoAI reloads in place, so a save under it usually applies at once: SoAI rebuilds the routing configuration and hands
it to the queue, the scheduler, the executor, the capacity service, and the Plugin Guardian. The exceptions matter
more than the rule.
MODELS.ROUTING.HTTP_CLIENT_LIMITS
and
MODELS.ROUTING.HTTP_CLIENT_TIMEOUTS
record a pending restart, because the shared HTTP client is built once at startup, and SoAI compares them against
the values the process started with rather than against the previous save, so restoring them clears the pending
restart they created. Lowering
MODELS.ROUTING.HEALTH_CHECKS.NUM_PLANNER_WORKERS
logs that it will apply on restart, while raising it spawns the extra workers immediately.
A second group is easier to miss, because those settings are read once while SoAI starts and never picked up by a
reload, and since they live under a reloadable section the WebUI reports nothing. Restart the process yourself after
changing
MODELS.ROUTING.DURABLE_QUEUE_HARD_LIMIT_TASKS,
MODELS.ROUTING.MIN_FREE_DISK_BYTES_FOR_ACCEPT,
MODELS.ROUTING.ACCEPTANCE_DB_BUSY_TIMEOUT_SEC,
MODELS.ROUTING.PLUGIN_PREFETCH_WINDOW_DEFAULT,
or
MODELS.ROUTING.HTTP_CLIENT_TRUST_ENV.
Everything under SYSTEM.INACTIVITY sits
outside the reloadable set and does record a pending restart.
Configuration scopes and restart behavior covers the reload rules for the
rest of the configuration.
Bound what SoAI accepts
MODELS.ROUTING.TASK_QUEUE_MAX_SIZE
defaults to 1000 and bounds the in-memory scheduling queue. A full queue produces an explicit refusal
rather than an unbounded wait, which
Resilience and self-healing describes. The value applies live, although
shrinking it while the queue is fuller than the new limit only takes the limit down to the current occupancy and
logs that it did. Keep it at 1 or above. Neither this key nor
MODELS.ROUTING.MAX_CONCURRENT_PLUGINS
has a minimum in the settings schema, so a 0 saves without complaint and then fails when SoAI rebuilds
the routing configuration, which makes it a way to break routing rather than a way to remove a cap.
Durable admission is checked before any of that, inside the database write that records the task.
MODELS.ROUTING.DURABLE_QUEUE_HARD_LIMIT_TASKS
caps non-terminal durable tasks and its factory value of 0 disables that ceiling,
MODELS.ROUTING.MIN_FREE_DISK_BYTES_FOR_ACCEPT
defaults to 268435456, or 256 MiB, measured on the volume that holds the system database, and
MODELS.ROUTING.ACCEPTANCE_DB_BUSY_TIMEOUT_SEC
gives that write 30 seconds to reach the database before acceptance fails. Set a positive durable limit when the
instance needs a queue ceiling, keep the three aligned with storage and recovery capacity, and restart after
changing any of them, because accepting more durable work than the instance can bring to a truthful terminal state
is worse than refusing it.
MODELS.ROUTING.HTTP_ASYNC_ACCEPT_DEFAULT
is false, so an HTTP inference request holds its connection unless the client sends
Prefer: respond-async. An asynchronous acceptance answers 202 with a body carrying the
accepted status and the task id, plus a Preference-Applied: respond-async header, and the caller
collects the result later. Turning the default on applies it to streaming requests as well and there is no header
that asks for the synchronous behavior back, so it suits automation that polls and breaks a client waiting on the
response body. A request that asks for its completion to be stored stays synchronous either way.
Limit what runs at once
| Control | Factory default | Owns |
|---|---|---|
MODELS.ROUTING.MAX_CONCURRENT_PLUGINS | 2 | Non-persistent plugin backends resident at the same time. |
MODELS.ROUTING.HEALTH_CHECKS.MAX_CONCURRENT_TASKS_PER_PLUGIN.DEFAULT | 4 | Running inference tasks on one plugin, for a plugin that declares no limit of its own. |
MODELS.ROUTING.OWNER_RUNNING_LIMITS.DEFAULT | 4 | Running inference tasks for one owner unless an owner-type override applies. |
The plugin limit has a shortcut in the WebUI: open Plugins, select Configure concurrent plugins on the Concurrent plugins card, change Maximum concurrent plugins, and select Save. It accepts a whole number from 1 through 1000, and saving it is a configuration change like any other, so it needs the same permission. Persistent plugins do not consume the limit, and Capacity planning explains how SoAI looks for a safe eviction candidate when every slot is occupied.
The per-plugin task limit is the one to read carefully, because a loaded plugin that declares its own concurrency wins over the configured value, which then applies only to a plugin that declares none. Every bundled plugin declares one: 1 for Ollama, MeloTTS, and Whisper, 2 for CTranslate2, 4 for Embedding and llama.cpp, 16 for vLLM, and 100 for External. Open Plugins, select the plugin, and read Max concurrent in Plugin information to see what a given plugin is actually enforcing.
MODELS.ROUTING.PROMPT_QUEUING is
false and adds a fourth cap when you turn it on, admitting at most 4 tasks at a time out of the
priority queue and into scheduling. That slot count is fixed and has no configuration path, so treat the setting as
on or off rather than as a number you can size.
Override one plugin or one owner type
Use
MODELS.ROUTING.HEALTH_CHECKS.MAX_CONCURRENT_TASKS_PER_PLUGIN.PER_PLUGIN
only for a measured plugin-specific difference, and only for a plugin that leaves the limit to SoAI. In Advanced
settings, search for that complete path and enter a JSON object keyed by plugin name:
{"my-plugin": 2}
An owner is the thing SoAI counts running work against, and there are four kinds: conversation,
http_request, mcp_client, and system.
MODELS.ROUTING.OWNER_RUNNING_LIMITS.PER_OWNER_TYPE
takes an object keyed by those names, so you can give API traffic a different ceiling from WebUI conversations
without touching the default. A limit of 0 or less removes the check for that owner type, which is
rarely what you want, since the limit exists so one account, script, or key cannot hold every running slot.
Size each plugin's queue
Each plugin also has its own bounded queue in front of its workers, and overflowing it refuses the dispatch rather
than growing. SoAI divides
MODELS.ROUTING.TASK_QUEUE_MAX_SIZE
by the number of plugins to get a share, raises that share to
MODELS.ROUTING.HEALTH_CHECKS.PLUGIN_QUEUE_MIN_SIZE,
which is 16, or to the plugin's concurrency limit multiplied by
MODELS.ROUTING.HEALTH_CHECKS.PLUGIN_QUEUE_BURST_FACTOR,
which is 4, whichever is largest, and caps the result at the global budget. Raise the burst factor when
a plugin with a small concurrency limit needs to absorb bursts, and raise the minimum size when many plugins are
dividing one queue budget between them.
The worker count follows the concurrency limit rather than the queue: a plugin runs one dispatcher worker per unit
of concurrency, capped by
MODELS.ROUTING.HEALTH_CHECKS.PLUGIN_QUEUE_MAX_WORKERS
at 32, so External with its declared 100 gets 32 workers rather than 100. A plugin that declares
unlimited concurrency instead gets
MODELS.ROUTING.HEALTH_CHECKS.PLUGIN_QUEUE_UNLIMITED_WORKERS,
which is 8.
Keep one busy plugin from crowding the others
MODELS.ROUTING.HEALTH_CHECKS.FAIR_DISPATCH_ENABLED
is true and engages only when every plugin slot is already occupied and another plugin is waiting for
capacity. While that holds, a non-persistent plugin that has reached its share stops receiving new dispatches until
the waiting plugins get theirs.
MODELS.ROUTING.HEALTH_CHECKS.FAIR_DISPATCH_MAX_PER_PLUGIN
is 32, but the share is the lower of that ceiling and the plugin's own concurrency limit, so on a
default instance the plugin limit is what binds and raising the ceiling changes nothing. Persistent plugins are
never throttled this way, and turning fairness off lets the priority order run unmodified, which favors whichever
plugin is already warm.
Plan and prefetch queued work
MODELS.ROUTING.HEALTH_CHECKS.NUM_PLANNER_WORKERS
defaults to 16 and sets how many workers pull tasks from the priority queue and turn them into
scheduling decisions. The minimum is 1. Raising it helps only when planning is the bottleneck rather than the
backends, and it is the one control here where the direction of the change matters.
MODELS.ROUTING.PLUGIN_PREFETCH_WINDOW_DEFAULT
defaults to 8 and limits how many claimed durable items SoAI keeps in memory per priority class ahead
of execution. A larger window gives the scheduler more to choose from at the cost of holding more leases, and the
drain loop reads it at startup along with its lease and sweep intervals, so the change lands on the next restart.
Set the deadlines an inference must meet
Several controls under HEALTH_CHECKS bound ordinary inference rather than probes, and these are the
ones that decide when a slow backend produces a timeout instead of an answer.
MODELS.ROUTING.HEALTH_CHECKS.NON_STREAMING_TIMEOUT_SEC
defaults to 300.0 and bounds a non-streaming attempt.
MODELS.ROUTING.HEALTH_CHECKS.STREAMING_IDLE_TIMEOUT_SEC
also defaults to 300.0 and bounds the gap between streamed chunks, so it governs a stalled stream
rather than a slow one.
The deadline for the first streamed chunk is not fixed, because a long prompt legitimately takes longer to prefill.
SoAI divides the prompt token count by
MODELS.ROUTING.HEALTH_CHECKS.STREAMING_PREFILL_MIN_TOKENS_PER_SEC,
which defaults to 8.0, adds
MODELS.ROUTING.HEALTH_CHECKS.STREAMING_PREFILL_OVERHEAD_SEC,
which is 60.0, and uses whichever is larger, that result or the non-streaming timeout. A per-request
timeout from the client caps the outcome. Lower the assumed rate if a host needs more time to prefill long
prompts. Existing configured values stay unchanged after an update. The admitted execution deadline covers
dispatch and the wait for meaningful output across retries; configuration changes apply to later requests.
Keepalives and empty chunks do not count as meaningful output.
Handle disconnects and duplicate work
MODELS.ROUTING.CANCEL_ON_CLIENT_DISCONNECT
defaults to false. When it is on and a streaming client goes away, SoAI publishes a cancellation for
that task, so the disconnect becomes a cancellation outcome instead of work that continues with nobody reading it.
Each stream reads the setting as it opens, so the change reaches new streams at once. Leave it off when a caller is
expected to reconnect or to collect accepted asynchronous work later, and remember that the request still ends only
as fast as the selected plugin honors cancellation.
MODELS.ROUTING.DEDUPLICATION_ENABLED
defaults to true, letting eligible identical requests share one lead execution while the rest wait on
its result. Streaming requests are excluded outright. By default the match covers the request payload, the resolved
execution targets, the priority, and the parameter snapshot, and
MODELS.ROUTING.DEDUPLICATION_KEYS
narrows the payload half to named fields when a field that does not affect the output keeps identical work apart.
Keep ordinary admission limits either way, because deduplication reduces only duplicate work.
Release idle model resources
SYSTEM.INACTIVITY.MODELS.ENABLED
is off by default, and what SoAI counts as activity is worth knowing before you turn it on. The monitor watches for
a model load, an inference request, or an embedding request, but only the MCP paths publish those two request
events, so a conversation in the WebUI and a call to the compatible API leave the timer running. On an instance
whose traffic arrives that way, the clock measures time since the last model load rather than time since the last
request.
When the setting is on, SoAI checks every
SYSTEM.INACTIVITY.CHECK_INTERVAL_SEC
seconds, 15 by default, and acts once the instance has been idle for
SYSTEM.INACTIVITY.MODELS.TIMEOUT_MINUTES,
which is 10. The action is the same force stop the administrator command performs rather than a quiet
unload, so it cancels running work and drains the queue, finalizing those tasks as failed with the reason that all
plugins are being stopped, and only then stops the plugins. Read that together with the activity rule above and
leave the setting off unless the deployment is genuinely idle between sessions or its traffic arrives over MCP.
System inactivity is a separate mechanism, also off by default, and it reads the same activity signal.
SYSTEM.INACTIVITY.SYSTEM.TIMEOUT_MINUTES
is 30,
SYSTEM.INACTIVITY.SYSTEM.ACTION
accepts shutdown or unload_models, and shutdown is the factory value, so
enabling
SYSTEM.INACTIVITY.SYSTEM.ENABLED
without changing the action stops the SoAI application after 30 minutes. It stops SoAI rather than the host, which
is a separate operation described in
System power and application lifecycle, and recovery means starting the service
again. Choosing unload_models runs the same force stop that model inactivity uses, and system
inactivity is evaluated first, so its action replaces the model action rather than running beside it.
Verify a change
- Record completed throughput, latency distribution, queue depth, rejections, plugin concurrency, memory, and provider limits.
- Change one control, and restart only if that control needs it.
- Repeat the same workload through steady state and recovery.
- Keep the change only if terminal outcomes and operating margin are still acceptable.