Service Autoscaling
Service autoscaling is an early-access feature. The syntax described here may evolve in future releases.
A workflow service normally runs as a
single, long-lived container. With an autoscaler block you can instead run a service as a pool
of replicas whose size grows and shrinks automatically based on metrics the service itself
exposes. This is useful for workloads whose demand varies over time — inference servers, queue
workers, or any service that benefits from running more (or fewer) copies as load changes.
The autoscaler decides when to add or remove a replica by evaluating PromQL expressions against the service’s own metrics. Replicas of an autoscaled service are reached through a single DNS name that always resolves to the currently-ready replicas, so clients do not need to know how many replicas exist at any moment.
When a service declares an autoscaler, Fuzzball:
- Pre-allocates capacity for up to
replicas.maxcopies of the service, startingreplicas.minof them (which may be0). - Scrapes the service’s metrics endpoint (when
metrics.enabledis set) at the configured interval. - Evaluates each
scale-uptrigger and thescale-downpolicy against those metrics. When ascale-uptrigger fires, the next pending replica is started; whenscale-downfires, a running replica is stopped. - Maintains a DNS record for the service that contains one address per ready replica, adding a replica’s address once it passes its readiness probe and removing it when the replica is scaled down.
Each scaling action is rate-limited by its cooldown-period, and the replica count is always
bounded by replicas.min and replicas.max.
The autoscaler evaluates PromQL expressions against metrics collected from your services. Fuzzball supports two types of metrics:
When you enable metrics.enabled: true on a service, Fuzzball scrapes a Prometheus-compatible
metrics endpoint from each replica at the configured interval. These are metrics that your service
application exposes — for example, vllm:num_requests_waiting from vLLM, or custom business metrics
from your application.
Configure the scrape with:
metrics.path— HTTP path to the metrics endpoint (default/metrics)metrics.port— TCP port where metrics are servedmetrics.interval— Scrape interval in seconds (default15)
All scraped metrics are tagged with service and replica labels, so your PromQL queries can
aggregate across the pool (e.g., sum(my_metric) by (service)) or target specific replicas.
For every autoscaled service (whether or not it exposes its own metrics), Fuzzball automatically
collects synthetic container-level metrics from the underlying runtime. These metrics are available
immediately without requiring your service to expose a /metrics endpoint, making them ideal for
simple CPU- or memory-based scaling policies.
The default metrics follow the cAdvisor naming convention:
| Metric name | Type | Description |
|---|---|---|
container_cpu_usage_seconds_total | Counter | Cumulative CPU time in seconds. Use rate(container_cpu_usage_seconds_total[1m]) to get current CPU cores in use. |
container_memory_usage_bytes | Gauge | Current memory usage in bytes. |
container_memory_limit_bytes | Gauge | Memory limit in bytes (omitted if the container has no effective limit). Useful for ratio-based policies like container_memory_usage_bytes / container_memory_limit_bytes. |
These metrics are collected from the container runtime at the same interval as service-exposed
metrics (or every 15 seconds if no scrape is configured) and are tagged with the same service and
replica labels.
Example: Scale up when average CPU usage across all replicas exceeds 80% of one core for one minute:
autoscaler:
replicas:
min: 1
max: 10
scale-up:
triggers:
- metrics-query: rate(container_cpu_usage_seconds_total[1m]) >= bool 0.8
cooldown-period: 60
Example: Scale down when a replica’s memory usage falls below 50% of its limit:
autoscaler:
scale-down:
metrics-query: |
(container_memory_usage_bytes / container_memory_limit_bytes) <= bool 0.5
cooldown-period: 300
Container metrics are synthetic — they are derived from container runtime stats rather than scraped from an endpoint. This means they appear in your PromQL queries just like scraped metrics, but your service does not need to export them.
Replicas of a self-scaling service share a single autoscaler DNS name of the form:
<service>.autoscaler.<workflow-id>.fuzzball
Resolving this name returns one address for every ready replica, so a client (or a load balancer) can distribute requests across the pool. Because the replicas are reached through this shared record, a self-scaling service cannot declare per-replica network endpoints.
The most common pattern is a service that scales its own replicas. Define replicas and one or
more scale-up triggers (with no services field) plus an optional scale-down policy:
version: v1
services:
inference:
image:
uri: docker://vllm/vllm-openai:latest
command: ["--model", "facebook/opt-125m", "--port", "8000"]
resource:
cpu:
cores: 8
memory:
size: 16GB
network:
ports:
- name: http
port: 8000
protocol: tcp
readiness-probe:
http-get:
path: /health
port: 8000
period-seconds: 5
autoscaler:
replicas:
min: 1
max: 8
metrics:
enabled: true
path: /metrics
port: 8000
interval: 15
scale-up:
triggers:
- metrics-query: vllm:num_requests_waiting >= bool 2
cooldown-period: 60
scale-down:
metrics-query: vllm:num_requests_running == bool 0
cooldown-period: 300
This service starts with one replica, scales up (one replica at a time, at most once per minute) whenever two or more requests are waiting, and scales back down — no more often than every five minutes — when a replica has no requests running.
Use the PromQLboolmodifier in comparisons (>= bool 2,== bool 0) so the query returns1when the condition holds and0when it does not. The autoscaler treats any non-zero result as a trigger to act.
Setting replicas.min: 0 lets a service idle with no running replicas and start its first replica
only when a trigger fires. This is well suited to bursty or on-demand workloads where you do not
want to hold capacity while the service is idle.
Instead of scaling its own replicas, a service may act as a source that scales one or more other services. This is useful for bringing a pool of workers up from zero in response to demand observed by a frontend.
To do this, give the source service’s scale-up triggers a services list naming the target
services. The targets define the replicas pool; the source does not:
version: v1
services:
frontend:
image:
uri: docker://mycompany/frontend:latest
network:
ports:
- name: metrics
port: 9090
protocol: tcp
autoscaler:
metrics:
enabled: true
path: /metrics
port: 9090
interval: 15
scale-up:
triggers:
- metrics-query: queue_depth >= bool 10
cooldown-period: 30
services: [worker]
worker:
image:
uri: docker://mycompany/worker:latest
autoscaler:
replicas:
min: 0
max: 20
scale-down:
metrics-query: worker_idle == bool 1
cooldown-period: 120
Here the frontend watches its queue depth and, when it grows, scales the worker pool up from
zero. Each worker owns its own scale-down policy, so it removes idle replicas independently of
the source.
A service in a workflow can render a configuration file from the live addresses of other services and have it injected into its container, then re-rendered automatically as those services’ replicas scale up and down. This lets a service (such as a load balancer or proxy) keep an up-to-date list of its backends without restarting.
Add a dynamic-config block naming the services to watch, the destination path inside the
container, and a script that emits the file contents. The addresses of each watched service are
exposed to the script as a bash array named SERVICES_<NAME> (the service name uppercased, with any
character that is not a valid identifier replaced by _):
services:
proxy:
image:
uri: docker://haproxy:latest
dynamic-config:
path: /usr/local/etc/haproxy/backends.cfg
services: [worker]
script: |
for ip in "${SERVICES_WORKER[@]}"; do
cat <<EOF
server ${ip} ${ip}:8080 check
EOF
done
Thescriptruns in a heavily restricted interpreter for safety: only bash builtins and heredocs are available. External commands (other than a no-argumentcatto support thecat <<EOFform) and any filesystem reads or writes are denied.
Fuzzball rejects a workflow at submission time if its autoscaler configuration is inconsistent. The most common rules are:
- A self-scaling service (no
scale-uptrigger targets other services) must definereplicasand must not define network endpoints. replicas.maxmust be at least1and not less thanreplicas.min.- A cross-service source (any
scale-uptrigger setsservices) must not define its ownreplicasor ascale-downpolicy; each target service owns those. - Every target named in a
serviceslist must exist, must defineautoscaler.replicas, and must not define its ownscale-uptriggers (a pool is scaled up by exactly one source). - All of a service’s
scale-uptriggers must agree: either every trigger targets other services, or none do. The two modes cannot be mixed within one service. - A given service may be targeted by at most one
scale-uptrigger. scale-downmay never target other services.dynamic-configmay only reference services that exist in the workflow.