On 4 September 2026, Amazon ECS added Early Success Criteria to rolling deployments. You set a percentage of healthy tasks and ECS marks the deployment successful when the target revision reaches it. With a desired count of 100 and a healthy percent of 90, the deployment succeeds at 90 tasks; the remaining ten launch through ordinary service scaling, outside the deployment.
The intent is sound. It is also the first time ECS has asked you to write down what your pipeline has been assuming for years: what “deployed” means. That answer has a rollback window attached.
What a completed deployment used to mean
Before this change, ECS completed a rolling deployment only after all of the following were true: the target revision reached 100 percent of the desired count with every task running and healthy; the circuit breaker or a CloudWatch alarm did not trigger a rollback; the bake time elapsed, if you use alarm-based rollback; and the source revision tasks were cleaned up.
Four conditions, none of them configurable. That rigidity is why deployments hang: long-lived connections hold the deployment open while the source tasks drain, constrained capacity leaves the last task waiting on hardware, and the pipeline run that triggered it queues everything else behind it.
The configuration
Early success criteria lives in the deployment configuration and applies only to the rolling strategy:
# bash
aws ecs update-service \
--cluster prod \
--service checkout-api \
--deployment-configuration '{
"strategy": "ROLLING",
"minimumHealthyPercent": 90,
"maximumPercent": 200,
"deploymentCircuitBreaker": { "enable": true, "rollback": true },
"earlySuccessCriteria": {
"enable": true,
"healthyPercent": 90,
"sourceServiceRevisionCleanup": "BLOCKING"
}
}'
healthyPercent is the share of the desired count that must be running and healthy on the target revision, rounded up. sourceServiceRevisionCleanup decides whether the old tasks go before or after success is declared. ECS always launches at least one task and waits for it to become healthy before it evaluates the percentage at all.
The arithmetic is unkind at low task counts
Rounding up means a percentage buys nothing until the service is wide:
| Desired count | Healthy percent | Required healthy tasks | Tasks saved |
|---|---|---|---|
| 2 | 50 | 1 | 1 |
| 3 | 50 | 2 | 1 |
| 10 | 80 | 8 | 2 |
| 10 | 100 | 10 | 0 |
| 100 | 90 | 90 | 10 |
On a three-task service, 50 percent removes one task from the critical path and leaves a third of the fleet on the old revision when the pipeline goes green. On a hundred-task service the same reasoning is defensible. Fleet width, not confidence, decides whether this is worth turning on.
The knob is coupled to your capacity floor
Here is the constraint that catches teams on the first attempt. healthyPercent must be greater than or equal to the service’s minimumHealthyPercent. For a replica service, minimumHealthyPercent defaults to 100 percent. On a default service, the only legal value for healthyPercent is 100, which is the behaviour you already had.
So enabling early success means first lowering minimumHealthyPercent, and that field does a different job. It is the floor on running, healthy tasks, and ECS uses it outside deployments too: when tasks go unhealthy the scheduler replaces them against the same floor, and if maximumPercent will not let a replacement start first, it stops unhealthy tasks one at a time using minimumHealthyPercent as the constraint.
Both horns are real. Leave the floor at 100 and the feature cannot be configured at all. Lower it to 90 to unlock healthyPercent: 90 and you have given the scheduler standing permission to run at 90 percent of desired count during every future unhealthy-task replacement, not just the deployment you wanted to shorten. One field, two jobs, and only one of them is the job you were reasoning about.
Where the rollback window ends
AWS states it plainly: after ECS completes the deployment, the circuit breaker and CloudWatch alarm rollback no longer apply, including while ECS launches the remaining tasks through regular service scaling. You cannot stop a deployment after it completes.

The circuit breaker has been the automatic rollback for ECS since December 2020, and became configurable on 1 July 2026 with a threshold set by fixed failure count or by percentage of desired count. Teams have spent six years treating it as a net under the whole rollout, which it was. healthyPercent is the point where the net is folded away and the last tasks land without it.
A deployment status only ever reports the threshold you gave it.
It is not a speed dial. It is the confidence level at which you stop paying for automatic protection, which is how AWS frames the use case: rollback protects the deployment until the target revision reaches a health level you define, and not after.
BLOCKING, DEFERRED, and the two-week tail
With BLOCKING, ECS cleans up the source revision tasks before declaring success. With DEFERRED, it declares success first and drains afterwards outside the deployment, retrying for up to two weeks.
DEFERRED is the right answer for long-lived connections or task scale-in protection, where draining is a long tail and the CI tool has a timeout. It also means two revisions serve production traffic after your pipeline reports green. Anything sequenced on deploy completion needs re-examining: a schema migration, a flag flip, a cache invalidation, a smoke test asserting a version header.
Your pipeline cannot tell the difference
Early success criteria adds no new deployment status. State-change events and CloudTrail show the same IN_PROGRESS to SUCCESSFUL lifecycle, and early completions appear when you filter ListServiceDeployments by SUCCESSFUL. Worse for anyone building a gate: after completion the task counts from DescribeServiceDeployments are a snapshot. The live view is DescribeServices.
A step that needs the full fleet has to ask the service, not the deployment:
# bash: wait for the fleet, not for the deployment
target_arn=$(aws ecs describe-services --cluster prod --services checkout-api \
--query 'services[0].deployments[?status==`PRIMARY`].id' --output text)
until [ "$(aws ecs describe-services --cluster prod --services checkout-api \
--query "services[0].deployments[?id=='$target_arn'].runningCount" --output text)" \
= "$(aws ecs describe-services --cluster prod --services checkout-api \
--query 'services[0].desiredCount' --output text)" ]; do
sleep 10
done
What Terraform cannot express yet
As of AWS provider v6.63.0 there is no early_success_criteria argument on aws_ecs_service. The deployment_configuration block carries strategy, bake_time_in_minutes, lifecycle_hook, and the linear and canary configurations. Everything else you need is expressible; this one field is not:
# hcl
resource "aws_ecs_service" "checkout_api" {
name = "checkout-api"
cluster = aws_ecs_cluster.prod.id
task_definition = aws_ecs_task_definition.checkout_api.arn
desired_count = 100
deployment_minimum_healthy_percent = 90 # must be lowered first
deployment_maximum_percent = 200
deployment_circuit_breaker {
enable = true
rollback = true
}
# deployment_configuration { early_success_criteria { ... } }
# not available in provider v6.63.0 -- set through the CLI, then assert in CI
}
A missing argument is a familiar inconvenience. The part worth testing is what happens on the next apply. In the provider’s update path, DeploymentConfiguration is built fresh whenever a field Terraform knows about changes, and populated only from those fields:
// internal/service/ecs/service.go, resourceServiceUpdate
if d.HasChanges("deployment_maximum_percent", "deployment_minimum_healthy_percent") {
if input.DeploymentConfiguration == nil {
input.DeploymentConfiguration = &awstypes.DeploymentConfiguration{}
}
input.DeploymentConfiguration.MaximumPercent = ...
input.DeploymentConfiguration.MinimumHealthyPercent = ...
}
Nothing reads the existing earlySuccessCriteria back off the service and carries it forward. Whether ECS preserves an omitted sub-field or clears it is not documented, so treat it as an open question on your own estate rather than settled behaviour. The cheap answer is to stop guessing and assert the setting in CI after every apply:
# bash: post-apply assertion
enabled=$(aws ecs describe-services --cluster prod --services checkout-api \
--query 'services[0].deploymentConfiguration.earlySuccessCriteria.enable' --output text)
if [ "$enabled" != "True" ]; then
echo "early success criteria was dropped from checkout-api" >&2
exit 1
fi
The same trade-off on Kubernetes
If you run Deployments rather than ECS services, none of this is new. It has never been written down as one setting. A rolling update spreads the same decision across three fields:
# yaml
spec:
progressDeadlineSeconds: 600 # when to stop watching
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 10% # the capacity floor, ~ minimumHealthyPercent
maxSurge: 25% # the headroom, ~ maximumPercent
maxUnavailable is the ECS capacity floor, maxSurge the upper limit. progressDeadlineSeconds, default 600, is the closest analogue to the moment ECS stops watching, and it surprises people: when it expires the Deployment gets a Progressing condition of False with reason ProgressDeadlineExceeded, and Kubernetes does not roll back. kubectl rollout undo is a decision somebody still has to make.
Argo Rollouts makes the choice explicit instead, promoting or aborting on measured signals rather than on pod readiness:
# yaml: the same decision, written down explicitly
spec:
progressDeadlineSeconds: 600
progressDeadlineAbort: true # abort the rollout, do not just stop watching
strategy:
canary:
steps:
- setWeight: 20
- analysis:
templates:
- templateName: success-rate
- setWeight: 100
The analysis step is the difference. ECS asks how many tasks are healthy; an analysis run asks whether the revision is behaving, and keeps asking after the traffic weight moves.
What we do about it in practice
We built production CI/CD from scratch for an early-stage security startup: NodeJS microservices in a monorepo, per-service independent versioning, builds that only touch the services a commit changed. GitHub Actions for CI, ArgoCD onto a new GKE cluster.
The slowest question there was not tool selection. It was what the pipeline was allowed to call deployed, per service, because with affected-only builds and independent versioning the answer genuinely differs between services. The rule we landed on is portable: no downstream step consumes the CD tool’s own status. Each service emits its own deployed signal after an explicit post-deploy check, and everything that follows waits on that.
ArgoCD Synced means the cluster matches the repository. Healthy means the resource passed a health assessment. Neither means the new revision is taking traffic. ECS SUCCESSFUL now means whatever you set healthyPercent to. The names differ, the gap is identical.
When not to turn this on
- Services with a low desired count. Below roughly ten tasks the rounding gives back one task and costs you a rollback window. Not a trade worth making.
- Services where
minimumHealthyPercentis deliberately 100. Lowering it to unlock early success changes behaviour outside deployments too. - Anything sequenced on deploy completion that assumes one revision is serving: migrations, flag flips, version-header assertions. Fix the sequencing first.
- Estates where deployment status feeds an automated gate nobody has re-read. The status does not change shape, so the gate will not tell you it now measures something else.
Good candidates are the ones AWS names: wide fleets, constrained capacity where the last tasks wait on hardware, and source revisions that drain slowly enough to time out a pipeline. On those, pick healthyPercent as the point where you would stop watching a rollout by hand.
Facing this on your stack? Naviteq’s senior platform team does this for SaaS, FinTech, and Enterprise teams across the US, EU, and Israel. Let’s talk.