diff --git a/cli/internal/manifest/manifest.go b/cli/internal/manifest/manifest.go
index eb680d2a..876a6238 100644
--- a/cli/internal/manifest/manifest.go
+++ b/cli/internal/manifest/manifest.go
@@ -220,8 +220,8 @@ func Validate(m Manifest) error {
if m.Service.StartCommand != nil && *m.Service.StartCommand == "" {
return errors.New("service.startCommand cannot be blank")
}
- if m.Service.Replicas < 1 || m.Service.Replicas > 10 {
- return errors.New("service.replicas must be between 1 and 10")
+ if m.Service.Replicas < 1 || m.Service.Replicas > 32 {
+ return errors.New("service.replicas must be between 1 and 32")
}
if m.Service.Placement == nil {
return errors.New("service.placement is required")
@@ -249,8 +249,8 @@ func Validate(m Manifest) error {
}
total += server.Count
}
- if total < 1 || total > 10 {
- return errors.New("service.placement manual total must be between 1 and 10")
+ if total < 1 || total > 32 {
+ return errors.New("service.placement manual total must be between 1 and 32")
}
if total != m.Service.Replicas {
return errors.New("service.placement manual total must equal service.replicas")
diff --git a/cli/internal/manifest/manifest_test.go b/cli/internal/manifest/manifest_test.go
index 57cdcbb7..b7c5103a 100644
--- a/cli/internal/manifest/manifest_test.go
+++ b/cli/internal/manifest/manifest_test.go
@@ -66,7 +66,7 @@ func TestPlacementRoundTripAndValidation(t *testing.T) {
{"blank server", &Placement{Mode: "manual", Servers: []PlacementServer{{ServerID: " ", Count: 1}}}, 1},
{"duplicate server", &Placement{Mode: "manual", Servers: []PlacementServer{{ServerID: "a", Count: 1}, {ServerID: "a", Count: 1}}}, 2},
{"nonpositive count", &Placement{Mode: "manual", Servers: []PlacementServer{{ServerID: "a", Count: 0}}}, 1},
- {"total exceeds limit", &Placement{Mode: "manual", Servers: []PlacementServer{{ServerID: "a", Count: 11}}}, 10},
+ {"total exceeds limit", &Placement{Mode: "manual", Servers: []PlacementServer{{ServerID: "a", Count: 33}}}, 32},
{"total differs from replicas", &Placement{Mode: "manual", Servers: []PlacementServer{{ServerID: "a", Count: 1}}}, 2},
}
for _, tc := range tests {
@@ -82,6 +82,26 @@ func TestPlacementRoundTripAndValidation(t *testing.T) {
}
}
+func TestReplicaLimit(t *testing.T) {
+ for _, placement := range []*Placement{
+ {Mode: "automatic"},
+ {Mode: "manual", Servers: []PlacementServer{{ServerID: "a", Count: 32}}},
+ } {
+ m := base()
+ m.Service.Replicas = 32
+ m.Service.Placement = placement
+ if err := Validate(m); err != nil {
+ t.Fatalf("32 replicas rejected: %v", err)
+ }
+ }
+
+ m := base()
+ m.Service.Replicas = 33
+ if err := Validate(m); err == nil {
+ t.Fatal("33 replicas accepted")
+ }
+}
+
func TestPlacementIsRequired(t *testing.T) {
_, err := Parse([]byte(`apiVersion: v1
service:
diff --git a/docs/services/scaling.mdx b/docs/services/scaling.mdx
index 13ab4163..1ed8c62b 100644
--- a/docs/services/scaling.mdx
+++ b/docs/services/scaling.mdx
@@ -7,7 +7,7 @@ description: "Replicas, placement, and server pinning."
Each service can run multiple replicas across your cluster. Configure how many replicas run on each server from the service settings.
-Replica count ranges from 1 to 10 per service.
+Replica count ranges from 1 to 32 per service.
## Serverless scaling
@@ -59,7 +59,7 @@ You can also manually lock any service to a specific server by setting the locke
- Stateful services are limited to 1 replica.
- Stateful services are always pinned to their locked server.
- Stateful services do not automatically fail over to another server.
-- Maximum 10 replicas per service.
+- Maximum 32 replicas per service.
- Serverless scaling requires a public HTTP service domain.
- Sleep and wake are proxy-local; serverless replicas must be placed on proxy nodes.
- Serverless traffic must be routed only to proxy nodes that own a local proxy replica for that service.
diff --git a/web/actions/projects.ts b/web/actions/projects.ts
index 27d3f370..204f73df 100644
--- a/web/actions/projects.ts
+++ b/web/actions/projects.ts
@@ -1067,7 +1067,7 @@ export type ServiceConfigUpdate = {
const placementInputSchema = z.discriminatedUnion("mode", [
z.strictObject({
mode: z.literal("automatic"),
- replicas: z.number().int().min(1).max(10),
+ replicas: z.number().int().min(1).max(32),
}),
z
.strictObject({
@@ -1076,7 +1076,7 @@ const placementInputSchema = z.discriminatedUnion("mode", [
.array(
z.strictObject({
serverId: z.string().min(1),
- count: z.number().int().min(1).max(10),
+ count: z.number().int().min(1).max(32),
}),
)
.min(1),
@@ -1092,10 +1092,10 @@ const placementInputSchema = z.discriminatedUnion("mode", [
path: ["placements"],
});
const total = value.placements.reduce((sum, item) => sum + item.count, 0);
- if (total > 10)
+ if (total > 32)
context.addIssue({
code: "custom",
- message: "Total replicas must be between 1 and 10",
+ message: "Total replicas must be between 1 and 32",
path: ["placements"],
});
}),
diff --git a/web/components/service/details/replicas-section.tsx b/web/components/service/details/replicas-section.tsx
index 80cec112..be1632c8 100644
--- a/web/components/service/details/replicas-section.tsx
+++ b/web/components/service/details/replicas-section.tsx
@@ -13,6 +13,7 @@ import {
EmptyTitle,
} from "@/components/ui/empty";
import { Input } from "@/components/ui/input";
+import { Slider } from "@/components/ui/slider";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type {
@@ -198,7 +199,7 @@ export const ReplicasSection = memo(function ReplicasSection({
setIsEditing(true);
setLocalReplicas((prev) => ({
...prev,
- [serverId]: Math.max(0, Math.min(10, Math.floor(value))),
+ [serverId]: Math.max(0, Math.min(32, Math.floor(value))),
}));
}, []);
@@ -211,7 +212,7 @@ export const ReplicasSection = memo(function ReplicasSection({
(sum, count) => sum + count,
0,
);
- setDesiredReplicas(Math.max(1, Math.min(10, manualTotal || 1)));
+ setDesiredReplicas(Math.max(1, Math.min(32, manualTotal || 1)));
}
setPlacementMode(nextMode);
};
@@ -268,7 +269,7 @@ export const ReplicasSection = memo(function ReplicasSection({
: null;
};
- const manualTotalIsValid = totalReplicas >= 1 && totalReplicas <= 10;
+ const manualTotalIsValid = totalReplicas >= 1 && totalReplicas <= 32;
if (service.stateful) {
return (
@@ -401,41 +402,36 @@ export const ReplicasSection = memo(function ReplicasSection({
{placementMode === "automatic" ? (
-
-
-
- The control plane distributes replicas evenly across healthy
- {service.serverlessEnabled ? " proxy nodes" : " nodes"} and
- moves them after failures.
-
+
+
+
Desired replicas
+
+ The control plane distributes replicas evenly across healthy
+ {service.serverlessEnabled ? " proxy nodes" : " nodes"} and
+ moves them after failures.
+