autoscaling block in ops.json and LocalOps
adds and removes replicas for you, between your service’s container count and a ceiling you set.
This is horizontal autoscaling - LocalOps runs more copies of your container, it does not give any single container
more CPU or memory. The CPU and memory each container gets stay exactly as you set them for the service, and are what
the utilization targets below are measured against.
This page assumes your service already deploys on LocalOps and you have set its CPU and memory requirements. If you
haven’t, start with creating a service.
Enable it
The smallest block that works needs a ceiling and one thing to scale on:ops.json
Autoscaling arguments
You must declare at least one of
target_cpu, target_memory or schedules. A replica range on its own has nothing
driving it, so the service would sit at its floor forever - LocalOps treats that as a mistake rather than a valid way to
say “off” and ignores the block.
Replica range
There is nomin_replicas key. The floor is always your service’s number of containers - the same count you set
when creating the service or later under its Scaling tab. That
count exists whether or not the service autoscales, so there is exactly one place to set it, and switching autoscaling
on never shrinks a running service by surprise. To raise or lower the floor, change the container count.
max_replicas is capped at the platform limit of 50. If it is below the service’s container count, it is raised to
match.
Scaling to zero is not supported. A service with no pods running has nothing to measure utilization on. If you stop a
service (container count
0), autoscaling is paused, not removed - the configuration is kept and applies again
once the count is raised above zero.Utilization targets
Bothtarget_cpu and target_memory are percentages of each pod’s request - the minimum CPU and memory you set for
the service - not its limit, and not the capacity of the underlying node.
ops.json
1 to 500. Values above 100 are legal and sometimes what you want: "target_cpu": 150 means “run
each pod at 1.5x the CPU it requested before adding another”. A target below 1 is ignored, as it would pin the service
at max_replicas permanently.
You can set both targets. LocalOps then scales on whichever one asks for more replicas, so neither resource gets
starved.
Time-of-day schedules
Some load is predictable enough that waiting for a metric to move is too late.schedules holds your service at a floor
for a window, so capacity is already there when traffic arrives:
ops.json
Schedule arguments
Schedules are deliberately not raw cron expressions. One
days list drives both the start and the end of a window, so a
window can’t open on weekdays and close every day by mistake.
Overnight windows
Ifend_time is at or before start_time, the window runs overnight and closes on the following day. For example,
"days": ["friday"] with "start_time": "22:00" and "end_time": "06:00" holds the floor from Friday 22:00 until
Saturday 06:00. LocalOps handles the day shift for you.
timezone has no default on purpose. A business-hours window without one would silently mean UTC, which is almost never
what was intended.
A schedule sets a floor, not a fixed count. Your utilization targets keep working inside the window and can take the
service above the scheduled number, up to max_replicas.
You can declare up to 12 windows per service. Beyond that, only the first 12 are kept. A window asking for more replicas
than max_replicas is capped at the ceiling, and one asking for fewer than the service’s container count is raised to
that count.
Proactive and reactive scaling
The two kinds of trigger behave differently, and most services want both.schedules are proactive. The window opens on a clock, so capacity is in place before the traffic it was meant for
arrives. Use them for load you can predict - business hours, a nightly batch, a weekly report run.
target_cpu and target_memory are reactive. Nothing moves until utilization has already risen, so there is always
a short lag between load arriving and replicas appearing. Use them for load you can’t predict, and as the safety net
above whatever your schedules reserved.
A common shape is a schedule that covers the working day and a CPU target that handles the spikes within it.
Where the new pods run
Adding replicas only helps if there is somewhere to put them. Every service runs on a node group you assign it to, and each node group has a minimum and maximum number of nodes. When your new replicas don’t fit on the nodes already running, the node group expands to make room, up to the maximum you set:- On AWS, the EKS managed node group backing your service adds EC2 instances of its configured instance type and capacity type (on-demand or spot), within its min and max size. An AWS node group holds between 2 and 20 nodes.
- On GCP, the GKE node pool backing your service adds nodes of its configured machine type, within its minimum and maximum node count.
New nodes take roughly 2 to 3 minutes to be provisioned, join the cluster and start running pods. This applies only
when new nodes are actually required. If the nodes already running have room to spare, your new replicas start
immediately and there is no wait at all - which is another reason to give schedules a little lead time on the traffic
you expect.
Setting it from the console instead
The same configuration can be set through the LocalOps console rather than committed to your repo. Both are equal sources, andops.json wins when both exist.
Precedence is all-or-nothing: an autoscaling block in ops.json replaces the stored configuration outright rather
than merging with it field by field. This keeps things predictable - your service can never end up half governed by
ops.json and half by a console someone else edited.
If your ops.json block can’t be used - a missing max_replicas, say - LocalOps falls back to the configuration stored
through the console rather than leaving the service unscaled, and says so in the deployment logs. A malformed push never
silently strips a service of autoscaling it already had.
When a service can’t autoscale
Some services are refused outright, whichever way the configuration was set. The deployment continues, and the reason is logged.- Preview services never autoscale. A preview’s
ops.jsoncomes off a pull request branch, so any PR author could otherwise raise your bill, and one stored ceiling would multiply across every open PR. Previews exist to validate a change, not to serve load. See ephemeral environments. - Helm chart services are scaled by their own chart values at install time.
autoscalingisn’t an accepted key in a helm service’sops.jsonat all. - Only web, worker and internal services can autoscale. Cron and job services aren’t long-running workloads, so there is nothing to scale on load.
- Services with a
diskvolume can’t autoscale. Durable storage means each replica owns its own volume, so replicas can’t be added on demand. Scratch space (mem) and mounted secrets are fine. See volumes.
Nothing here fails a deployment
Likedeploy_timeout, a value outside the allowed range is corrected
rather than treated as a broken deployment. Every correction is logged.
A block that couldn’t work at all - no
max_replicas, no trigger, or a utilization target with no matching resource
request - is ignored entirely, with the reason in your deployment logs.
Verify it worked
Check your deployment logs. Every value that was clamped, every schedule dropped, every fallback to the console configuration and every reason a block was refused is written there, naming which source it came from. Deployment logs are where you confirm the numbers you declared are the numbers in force. You can also see the replicas actually running for a service in the LocalOps console, shown as(x/y) - x running out
of y total.