boundless-author-7811
02/24/2026, 10:54 AMtall-airline-16756
02/24/2026, 10:54 AMtall-airline-16756
02/24/2026, 10:55 AMnumProcs := runtime.NumCPU() and spawn that many goroutines.
3. Use atomic.AddUint64(&start, step) to claim the next chunk.
4. Each worker loops over the claimed indices and performs work.
5. Wait for all workers via sync.WaitGroup.
Example pattern
var start uint64 = 0
step := uint64(2) // tuned step; small chunks for load balancing
end := uint64(totalWork) // totalWork = number of downtracks, packets, etc.
numProcs := runtime.NumCPU()
var wg sync.WaitGroup
wg.Add(numProcs)
for p := 0; p < numProcs; p++ {
go func() {
defer wg.Done()
for {
n := atomic.AddUint64(&start, step)
if n >= end + step {
return
}
for i := n - step; i < n && i < end; i++ {
// perform unit work, e.g. writeRTP(i)
}
}
}()
}
wg.Wait()
Tuning guidance
• Step size: target each iteration to take ~50–100µs. For operations that take ~50µs per unit, a step of 2 worked well in LiveKit benchmarks. Larger per-unit work => larger step.
• Worker count: use runtime.NumCPU() to avoid oversubscription.
• Fallback: for very small numbers of tasks, run a simple serial loop to avoid overhead.
Why this works
• Atomic increments are cheaper than per-task channels and avoid frequent goroutine scheduling.
• Small chunks let faster workers pick more work; no static assignment means no worker waiting on a slower peer.
• One goroutine per CPU prevents oversubscription and keeps scheduling predictable.
When not to use this
• If tasks are heavy and long-running, a simpler fixed-assignment or dedicated worker-per-task model may be fine.
• If tasks must preserve strict ordering across all units, additional coordination will be needed.
If you want, I can:
• Provide a ready-to-run Go function wrapping this pattern for your specific workload (e.g., RTP forwarding, batch processing).
• Help pick a step size based on a sample per-unit runtime you can paste here.
• Show a version that supports graceful shutdown, error collection, or per-worker profiling.
Which of these would be most helpful next?
Sources: Going beyond a single-coreboundless-author-7811
02/24/2026, 10:58 AMtall-airline-16756
02/24/2026, 10:58 AMproc.userdata. All sessions running in that process reuse those resources.
• Why use it: Avoids repeated expensive initialization per session; keeps resource lifecycle tied to process lifecycle which is easier to manage.
Example pattern
1. In your AgentServer setup_fnc, create and attach the shared resource(s) to proc.userdata.
2. Use those prewarmed resources inside session handlers via ctx.proc.userdata.
3. Clean up in process shutdown if needed.
Sketch:
# in server setup
def prewarm(proc: JobProcess):
proc.userdata["model"] = MyHeavyModel.load()
# optional: create a WorkerPool object here and start it
proc.userdata["pool"] = WorkerPool(num_workers=4)
asyncio.create_task(proc.userdata["pool"].start())
server = AgentServer(setup_fnc=prewarm)
Session usage:
@server.rtc_session(agent_name="worker-agent")
async def entrypoint(ctx: JobContext):
model = ctx.proc.userdata["model"]
pool = ctx.proc.userdata["pool"]
# submit async work to the pool or run directly
await pool.submit(lambda: model.predict(some_data))
# ensure graceful cleanup for session-local tasks
async def on_shutdown():
await pool.shutdown() # or other cleanup actions
ctx.add_shutdown_callback(on_shutdown)
...
Explicit asyncio.Queue worker pool (fine-grained control)
• What it is: An async in-process queue with N worker coroutines consuming jobs. Useful when you need task scheduling, backpressure, or to limit concurrency for I/O-bound tasks.
• Where to put it: Create it in setup_fnc and store it on proc.userdata, so it’s shared by sessions in that process.
• Lifecycle: Start it once when process starts; on process/session shutdown, drain the queue then cancel worker coroutines.
WorkerPool class (pattern)
import asyncio
class WorkerPool:
def __init__(self, num_workers):
self.queue = asyncio.Queue()
self.workers = []
self.num_workers = num_workers
self._started = False
async def worker(self):
while True:
job = await self.queue.get()
try:
await job()
except asyncio.CancelledError:
break
except Exception as e:
# handle job error (logging, metrics)
pass
finally:
self.queue.task_done()
async def start(self):
if self._started:
return
self._started = True
for _ in range(self.num_workers):
self.workers.append(asyncio.create_task(self.worker()))
async def submit(self, coro_func):
await self.queue.put(coro_func)
async def shutdown(self, wait_timeout: float = 10.0):
# wait for queued work to finish
try:
await asyncio.wait_for(self.queue.join(), timeout=wait_timeout)
except asyncio.TimeoutError:
# not all tasks finished in time
pass
# cancel workers
for w in self.workers:
w.cancel()
await asyncio.gather(*self.workers, return_exceptions=True)
Lifecycle integration tips
• Start pool in setup_fnc so it is created once per process. Use asyncio.create_task(pool.start()).
• Use ctx.add_shutdown_callback() inside sessions to cancel any session-specific background tasks and to ensure orderly shutdown.
• For process termination (e.g., container termination), ensure your environment allows a grace period (Kubernetes terminationGracePeriodSeconds) so shutdown() can drain queued jobs.
• Avoid creating separate thread pools per session — prefer sharing per-process resources via proc.userdata.
Sizing and tuning
• Workers: For I/O-bound tasks, you can have more workers than CPU cores; for CPU-bound tasks, use roughly num_cpus workers.
• Backpressure: Limit queue size (wrap submit() to reject or block when queue is full) if producers can outpace consumers.
• Job size: If jobs are short and frequent, prefer many small async tasks; if jobs are long/blocking, run them via run_in_executor or isolate in separate processes.
Error handling and observability
• Ensure errors inside jobs are caught and logged; consider a poison-job policy to avoid repeated failures.
• Add metrics for queue length, job latency, worker errors so you can tune pool size and timeouts.
• If you need tracing per job, propagate context or use a lightweight job wrapper that attaches trace IDs.
Graceful shutdown flow
1. Stop accepting new jobs (e.g., set a flag or stop producers).
2. Wait for queue to drain with a timeout.
3. Cancel worker coroutines.
4. Close shared resources (model, DB connections, HTTP clients).
5. Exit process.
Next steps I can help with
• Provide a complete runnable example integrating this pool into your AgentServer and session code.
• Recommend sizing and queue limits if you share whether tasks are CPU- or I/O-bound and an average task duration.
• Show an example that uses run_in_executor for blocking CPU-bound work.
Tell me which option you want (prewarmed resources vs. explicit queue pool) and a few details about your workload, and I’ll produce a concrete, runnable example tailored to your use case.
Sources: Gemini Realtime Agent with Live Vision | LiveKit Documentation | Collect recording consent with tasks | LiveKit Documentation | Job lifecycle | LiveKit Documentation | Self-hosted deployments | LiveKit Documentationboundless-author-7811
02/25/2026, 5:25 AMtall-airline-16756
02/25/2026, 5:25 AMboundless-author-7811
02/25/2026, 5:29 AMtall-airline-16756
02/25/2026, 5:29 AMboundless-author-7811
02/25/2026, 6:29 AMtall-airline-16756
02/25/2026, 6:29 AMboundless-author-7811
02/25/2026, 3:35 PMtall-airline-16756
02/25/2026, 3:36 PMboundless-author-7811
02/25/2026, 4:21 PMtall-airline-16756
02/25/2026, 4:22 PMnum_idle_processes setting for LiveKit in self‑hosted mode.
Short answer:
• There is no documented default value for num_idle_processes in the sources. If you don’t explicitly configure it, the system behaves as if num_idle_processes = 0 (no prewarmed processes kept ready).
• You must set num_idle_processes in your AgentServer configuration (or equivalent deployment config) to keep prewarmed worker processes available for faster session start.
What this parameter does
• num_idle_processes controls how many prewarmed job processes are kept idle and ready to handle incoming jobs.
• Prewarming is useful for loading heavy resources once per process (models, clients, connection pools) via the prewarm / setup_fnc hook, reducing per-job startup latency.
Practical guidance for self‑hosted deployments
• If you want faster session startup and can afford the memory/CPU, set num_idle_processes > 0 so processes load models at startup and stay ready.
• If you leave it unset (or set to 0), each job will start fresh and perform its own initialization, which increases per-job latency but reduces idle resource usage.
• Tune the value based on:
◦ Memory/CPU cost per prewarmed process (models loaded, connection pools).
◦ Expected incoming job rate and acceptable startup latency.
◦ Pod sizing and instance limits in your deployment environment.
If you’d like, I can:
• Show a code snippet for where to set num_idle_processes in your AgentServer setup (Python or Node), or
• Help pick a starting value based on your per-process memory footprint and expected request rate.
Which would you prefer?
Sources: Server options | LiveKit Documentation | Simple Content Filter | LiveKit Documentation | Agents (Python) | livekit/livekit_composite | DeepWikiboundless-author-7811
02/25/2026, 4:23 PMtall-airline-16756
02/25/2026, 4:23 PMnum_idle_processes is mandatory and what happens if you don’t configure it.
• It is not mandatory to set num_idle_processes.
• If you do not configure it, the system behaves as if `num_idle_processes = 0`: no prewarmed idle job processes are kept. Jobs will start fresh and perform their own initialization when dispatched.
• That means no idle/prewarmed worker processes are spun just for waiting; workers are created on demand per job (subject to whatever process lifecycle your AgentServer deployment or runtime enforces).
Practical notes
• Leaving it at 0 reduces idle memory/CPU usage but increases per-job startup latency (since heavy resources like models must be loaded at job start).
• If you want faster session startup, set num_idle_processes > 0 and size it based on the memory/CPU cost per prewarmed process and your expected job arrival rate.
If you’d like, I can show a short code snippet for setting num_idle_processes in your AgentServer setup (Python or Node) and help pick a starting value based on your per-process resource footprint. Which language do you use?boundless-author-7811
02/25/2026, 4:28 PMnum_idle_processes sets the numbers of workers?tall-airline-16756
02/25/2026, 4:28 PMnum_idle_processes sets the number of worker processes.
Short answer
• num_idle_processes controls how many prewarmed (idle) job processes the AgentServer keeps ready to accept new jobs. In that sense it does set how many idle worker processes are kept spun up.
• It does not necessarily define the absolute total number of worker processes the server may use over time; it only specifies how many idle/prewarmed processes are maintained for immediate use. Additional processes may still be created on demand according to the server’s runtime behavior (i.e., jobs started without an idle process will spawn processes as needed).
Behavior summary
• If you set `num_idle_processes = N`: the server will prewarm and maintain N idle job processes (each running your `prewarm`/`setup_fnc`) so new sessions can start with low latency.
• If you do not set it (or set it to 0): no prewarmed idle processes are kept. Workers are created on demand when jobs arrive, which increases startup latency but conserves idle CPU/RAM.
• It is not mandatory to set this value.
Practical note
• Treat num_idle_processes as the number of ready-to-serve processes you want kept warm. Pick it based on the memory/CPU cost of a prewarmed process and your expected job arrival rate (more arrivals → larger value for lower latency; constrained resources → smaller or zero).
• If you need, I can show a short code snippet for setting num_idle_processes in your AgentServer setup (Python or Node) and help pick a starting value based on your process memory footprint and expected arrival rate. Which language do you use?boundless-author-7811
02/26/2026, 5:42 AMnum_idle_processes . How do they work together and what is their dependencetall-airline-16756
02/26/2026, 5:43 AMnum_idle_processes relate, how they work together, and what depends on what. Below is a concise, practical explanation with lifecycle and operational notes so you can tune behavior for latency, cost, and resiliency.
High-level roles
• AgentServer
◦ The central process that accepts incoming jobs/sessions (RTC sessions, agent tasks) and coordinates dispatch.
◦ Responsible for lifecycle management of jobs and for applying your setup_fnc / prewarm logic when workers start.
• Worker processes (job processes)
◦ Separate child processes (or runtime units) that actually run agent sessions and execute job logic.
◦ Each worker loads any heavy resources in its prewarm/setup step (models, clients, connection pools) and holds per-process state in proc.userdata.
◦ Workers execute sessions in isolation from each other so one session’s crash or memory pressure won’t necessarily affect others in a different process.
• num_idle_processes
◦ A configuration that tells the AgentServer how many worker processes to keep prewarmed and idle, ready to accept new jobs immediately.
◦ These idle workers run your setup_fnc so heavy resources are already loaded, reducing per-job startup latency.
How they work together (lifecycle)
1. Startup
◦ AgentServer starts. If num_idle_processes > 0, it spawns that many worker processes and runs the prewarm/setup function in each. Those processes sit idle, waiting for assignments.
1. Job arrival
◦ When a new job/session arrives, AgentServer:
▪︎ Preferentially assigns it to an idle/prewarmed worker (fast path).
▪︎ If no idle worker is available, it may spawn a new worker on demand (slow path) or queue the job depending on your runtime’s behavior.
1. During job execution
◦ The assigned worker runs the job; heavy resources from prewarm are reused via proc.userdata.
1. Job completion
◦ After the job finishes, the worker either:
▪︎ Returns to the idle pool (if you keep it alive), or
▪︎ Terminates (if it was created for that job and not kept idle).
◦ AgentServer keeps num_idle_processes workers ready. If a worker died or was used up, AgentServer may spawn new idle workers to maintain that count.
1. Shutdown / scaling down
◦ AgentServer or orchestrator terminates workers; ensure graceful shutdown callbacks are used to stop background tasks and allow in-flight jobs to finish.
Dependencies and important interactions
• AgentServer depends on workers to run actual sessions; it orchestrates spawning, assignment, and cleanup.
• Workers depend on AgentServer for job assignments and for lifecycle signals (start, shutdown).
• num_idle_processes affects:
◦ Latency: higher values reduce cold-start time per job because workers are already warmed.
◦ Resource usage: each idle worker consumes RAM/CPU (models, threads, file descriptors). More idle processes → higher idle cost.
◦ Throughput readiness: if jobs arrive in bursts, a larger idle pool absorbs bursts without spawning new processes.
• Worker lifecycle depends on process-level prewarm and your cleanup hooks; make sure you register shutdown callbacks so workers stop background tasks and free resources.
Practical trade-offs and tuning
• Latency vs. cost
◦ Set num_idle_processes > 0 to reduce startup latency at the expense of idle resource usage.
◦ Set it to 0 to minimize idle cost but accept higher latency as workers are created on demand.
• Resource sizing
◦ Estimate memory/CPU consumed by a prewarmed worker (models, clients). Divide available host resources by that to get a practical upper bound on prewarmed workers per node.
• Arrival pattern
◦ If traffic is steady, smaller idle pools work because workers will be reused.
◦ If traffic is bursty, keep enough idle workers to cover typical burst size, or combine a small idle pool with fast on-demand worker creation.
• Failure and resilience
◦ Workers are isolated: one worker crash normally doesn’t crash AgentServer or other workers, but AgentServer must detect the crash and replace the worker to maintain num_idle_processes.
• Graceful shutdown
◦ Use shutdown callbacks in workers and set container terminationGracePeriodSeconds long enough to finish in-flight jobs (e.g., > average job duration).
Concrete examples
• Low-cost, low-latency need: num_idle_processes = 2–4 per AgentServer instance if each prewarmed worker uses modest RAM/CPU.
• High-burst workload: set idle count to match expected burst concurrency, or use autoscaling to add more AgentServer instances plus idle workers.
• Memory-constrained node: if one prewarmed worker uses 1.5 GB, then num_idle_processes should be 0 or 1 on a 2 GB node to avoid OOMs.
Next step
• If you want, I can:
◦ Show the code snippet for setting num_idle_processes in Python or Node.
◦ Help pick a starting value given the per-worker memory/CPU footprint you see in a quick local test.
◦ Recommend an autoscaling approach (HPA rules + custom concurrency metrics) to combine prewarmed workers with horizontal scaling.
Which would you like to do next?boundless-author-7811
02/26/2026, 5:45 AMtall-airline-16756
02/26/2026, 5:45 AMnum_idle_processes only controls how many prewarmed (idle) workers are kept ready; it does not by itself cap the total workers that can be created on demand.
How it works in practice
• Prewarmed idle workers:
◦ If you set num_idle_processes = N, the AgentServer will pre-spawn and keep N worker processes warmed (running your `prewarm`/`setup_fnc`) so incoming jobs can be assigned immediately.
◦ Those N processes consume RAM/CPU while idle.
• On-demand workers:
◦ When no idle worker is available, the AgentServer may spawn additional worker processes to handle incoming jobs (slow path). The sources describe this behavior conceptually but do not specify a fixed upper bound.
◦ How many on-demand workers can be started depends on:
▪︎ Host resources (how many processes your node can run without OOM/CPU exhaustion).
▪︎ Any process- or orchestration-level limits you configure (max pods per node, container CPU/memory requests & limits, quotas).
▪︎ Your AgentServer or deployment configuration (some deployments may impose a cap or use autoscaling to add more AgentServer instances instead of unlimited per-instance workers).
• Worker lifecycle:
◦ After a job completes, a worker may return to the idle pool (to maintain num_idle_processes) or exit depending on how you’ve configured lifecycle behavior and resource policies.
Practical guidance for sizing and limits
• Estimate per-worker resource use (RAM/CPU with prewarmed models). Divide node capacity by that footprint to determine a safe upper bound of concurrent worker processes on that host.
• Use num_idle_processes to trade latency vs. idle cost: choose N so prewarmed workers cover expected bursts without exceeding host capacity.
• Protect the node: set container resource requests/limits and use orchestration-level quotas so worker spawning cannot exhaust the node.
• Prefer horizontal scaling: instead of letting one AgentServer spawn an unbounded number of workers, scale AgentServer pods horizontally (with each maintaining a reasonable idle pool). Use autoscaling policies (CPU, concurrency, or custom metrics) to keep latency and resource usage controlled.
• Add a concurrency limit inside the AgentServer or orchestration rules so it won’t create more workers than the node can sustain.
If you want, I can:
• Help compute a safe per-node worker cap if you share the per-worker RAM/CPU footprint.
• Provide an example config pattern (num_idle_processes, container limits, and HPA) that balances prewarm latency with node stability. Which would you like?boundless-author-7811
02/26/2026, 6:20 AMtall-airline-16756
02/26/2026, 6:21 AMnum_idle_processes interact, plus a short textual explanation.
Copy the Mermaid code below into any Mermaid renderer (e.g., Mermaid Live Editor, VS Code Mermaid preview, or documentation that supports Mermaid) to view the diagram.
Mermaid flowchart
flowchart TD
subgraph AgentServerNode["AgentServer"]
A[Start AgentServer]
B{num_idle_processes > 0?}
C[Spawn N idle workers\n(run prewarm/setup_fnc)]
D[Listen for incoming jobs]
end
subgraph WorkerPool["Worker Processes"]
direction TB
I[Idle Worker (prewarmed)\n(proc.userdata loaded)]
J[On-demand Worker (spawned)\n(initializes at job start)]
K[Running Job\n(session/job executing)]
L[Return to idle pool\nor exit]
end
subgraph Orchestration["K8s / Host / Limits"]
O[Host CPU / RAM limits]
P[Container resource limits / quotas]
Q[Autoscaler / HPA]
end
A --> B
B -- yes --> C
B -- no --> D
C --> I
D -->|job arrives| R{Is idle worker available?}
R -- yes --> I --> K
R -- no --> S[Spawn on-demand worker] --> J --> K
K --> T{Job complete}
T -- keep warm --> L --> I
T -- terminate worker --> L -->|exit| U[Worker process ends]
U --> V[AgentServer may spawn new idle worker to maintain count]
O -->|limits workers| S
P -->|enforces limits| S
Q -->|scale AgentServer instances| A
%% Explanatory labels
R:::decision
S:::action
classDef decision fill:#f9f,stroke:#333,stroke-width:1px;
classDef action fill:#bbf,stroke:#333,stroke-width:1px;
Short explanation (how the pieces work together)
• AgentServer is the central coordinator that accepts jobs and manages worker processes.
• If num_idle_processes is set, AgentServer pre-spawns that many idle workers and runs your `prewarm`/`setup_fnc` so heavy resources (models, clients) are loaded into proc.userdata.
• When a job arrives, AgentServer assigns it to an idle worker if available (fast path). If none are idle, it may spawn an on-demand worker (slow path) which initializes at job start.
• After a job completes, the worker either returns to the idle pool (to maintain num_idle_processes) or exits, freeing resources.
• The total number of workers that can exist depends on host/container resource limits and any autoscaling rules; num_idle_processes only sets how many are kept prewarmed and ready.
• Kubernetes / host limits and HPA policies influence how many workers/AgentServer instances you can practically run.
If you’d like, I can:
• Produce a variant that shows lifecycle timing (startup, job assignment, shutdown) or
• Produce a version with recommended values/annotations (e.g., where to set terminationGracePeriodSeconds, where to enforce concurrency limits). Which would you prefer?boundless-author-7811
02/26/2026, 9:31 AMtry:
await asyncio.wait_for(
self.lkapi.agent_dispatch.create_dispatch(
CreateAgentDispatchRequest(
agent_name=self.agent_name,
room=room_name,
metadata=json.dumps({
"trunk_id": trunk_id,
"phone_number": phone_number,
"customer_data": customer_row,
"caller_phone_id": phone_number_id,
"resource_type": "phone"
})
)
),
timeout=10.0
)
<http://logger.info|logger.info>(
"✅ Call successfully dispatched to LiveKit for customer %s (room: %s)",
customer_id,
room_name
)
return Truetall-airline-16756
02/26/2026, 9:32 AMcreate_dispatch call was rejected by LiveKit and how to handle the possible outcomes in your scheduler code.
Short summary
• A server-side rejection or error when calling create_dispatch will surface as an exception from the SDK/network layer. You must catch and inspect exceptions (and timeouts) to know the call failed.
• A successful create_dispatch only means the dispatch request was accepted by LiveKit’s dispatch system (job queued/created). It does not guarantee the agent will accept the job later — agent-level rejection is a separate runtime event inside the agent and isn’t reported by create_dispatch.
• To detect agent-level rejection, you must observe job lifecycle events (logs, callbacks, or monitoring) after the dispatch is created.
Concrete changes to your snippet 1) Add exception handlers for timeout and API errors. 2) Log error details (message, status/code, metadata) so you can distinguish rejected vs transient network errors. 3) Optionally inspect the returned response (on success) for confirmation data.
Example pattern (adapt to the exact exception types your SDK exposes):
from asyncio import TimeoutError
# replace ApiError with the SDK-specific error class (e.g., TwirpError)
from livekit import api # or correct module for your SDK
try:
resp = await asyncio.wait_for(
self.lkapi.agent_dispatch.create_dispatch(
CreateAgentDispatchRequest(... )
),
timeout=10.0
)
<http://logger.info|logger.info>("✅ Call successfully dispatched to LiveKit for customer %s (room: %s)",
customer_id, room_name)
logger.debug("Dispatch response: %s", resp)
return True
except TimeoutError:
logger.error("❌ create_dispatch timed out after 10s for customer %s (room: %s)",
customer_id, room_name)
return False
except api.TwirpError as e:
# SDK-specific structured error: log code and metadata if available
logger.error("❌ create_dispatch rejected by server: %s; code=%s; meta=%s",
getattr(e, "message", str(e)),
getattr(e, "code", None),
getattr(e, "metadata", None))
return False
except Exception as e:
# catch-all for network errors, serialization errors, etc.
logger.exception("❌ Unexpected error when calling create_dispatch: %s", e)
return False
Notes about agent-level rejection vs dispatch failure
• create_dispatch success: means the request was accepted (job created/queued). It does not mean the agent processed the job successfully.
• Agent runtime rejection: if the agent code rejects the job (for example, job.reject(...)), that rejection happens after dispatch and is not returned by create_dispatch. To detect that:
◦ Monitor agent logs/metrics for job rejection events.
◦ Use any job lifecycle callbacks or webhooks your deployment exposes to report job acceptance/rejection.
◦ Add tracing/metadata you can correlate between the dispatch response and later agent events.
If you need help identifying the right exception class
• Tell me which LiveKit Python package or client you’re using (package name and version). I’ll show the exact exception types to catch (e.g., TwirpError or a client-specific APIError) and example logging fields to inspect.
Want help adding retries or backoff?
• If you’d like, I can give a retry/backoff pattern for transient failures (timeouts/network errors) while avoiding retries for hard rejections (HTTP 4xx-like errors). Which behavior do you prefer? Sources: Agent dispatch | LiveKit Documentation | Agents telephony integration | LiveKit Documentation | sip | livekit/livekit_composite | DeepWiki | Why do jobs stop after agent rejection? | LiveKitboundless-author-7811
02/26/2026, 10:14 AMtall-airline-16756
02/26/2026, 10:14 AMload function should depend on the number of pods.
Short answer
• The load function is evaluated per AgentServer instance (per pod). It should reflect the local pod’s current load and capacity, not the total number of pods.
• Cluster-level scaling decisions should aggregate per-pod load signals (or use a separate metric) to decide how many pods to run.
Why per-pod load matters
• The load function tells the scheduler how busy a particular AgentServer pod is (e.g., how many active jobs, CPU usage, memory pressure, token throughput).
• The scheduler or dispatcher uses that per-pod load to choose where to dispatch new jobs so work is balanced across available pods.
• Keeping load scoped to the pod makes scheduling decisions local and immediate, and avoids coupling the scheduler to cluster topology.
How pod count fits in
• Pod count is an outcome of autoscaling or your deployment plan — it is not something a single pod’s load function should rely on.
• An autoscaler (HPA, custom controller) should observe per-pod loads (or aggregated metrics) and increase/decrease pod count when needed. That controller uses the per-pod load metrics to scale horizontally.
Practical guidance — what your load function should report
• Report metrics that reflect the pod’s capacity to accept more jobs:
◦ Active/in-flight job count or fraction of max concurrent jobs.
◦ CPU utilization or memory pressure.
◦ Token consumption rate relative to local or global quotas (if relevant).
◦ Any custom capacity constraints (e.g., open connections, model instances).
• Return a normalized value the dispatcher understands (for example: 0.0 = idle, 1.0 = full).
• Keep the calculation local and conservative (avoid returning overly optimistic low load).
How to use those signals for scaling
• Aggregate per-pod loads (average, p95, or custom) in your metrics backend.
• Configure autoscaling rules to:
◦ Scale out when average load exceeds a threshold (e.g., 0.7) or when queue length grows.
◦ Scale in only when load is low for a sustained period and in-flight jobs are near zero (to allow graceful draining).
• For bursty workloads, consider combining a small prewarmed num_idle_processes per pod with autoscaling to add pods for larger bursts.
Example patterns
• Per-pod load = active_jobs / max_concurrent_jobs. Dispatcher avoids pods near load=1.0.
• HPA uses the average of that metric across pods; if avg_load > 0.7 → add pods.
• Use terminationGracePeriodSeconds >= longest expected job duration so pods can drain safely when scaling in.
When (and how) pod count can influence local decisions
• You might expose a cluster-level hint (e.g., global token quota left) so each pod can be more conservative when global capacity is low. But this is an external signal — it shouldn’t be baked into the per-pod load logic.
• Example: if global LLM tokens remaining are low, pods can raise their reported load artificially so dispatcher routes fewer jobs, avoiding a global quota breach.
Bottom line
• Keep load per-pod and focused on local capacity. Use aggregated metrics and autoscaling logic to decide how many pods are needed overall.
• If you want, I can suggest a specific load function formula (normalized value) based on your metrics (in-flight jobs, CPU, memory, tokens/sec). Share which signals you have available and I’ll recommend one. Which signals can you expose?