Overview

There is a recurring pattern I keep seeing with side projects and small internal tools. The application is fine. The container is fine. The AWS part is what slows everything down.

Not because ECS is bad. Quite the opposite: plain ECS gives you a lot of control. But that control comes with a lot of setup. Subnets, load balancer, listener rules, security groups, scaling policies, logging, IAM roles, health checks, certificates. For a production platform team this is normal life. For one container and one weekend project, it is a lot.

In late November 2025, AWS quietly introduced Amazon ECS Express Mode. The idea is refreshingly simple: you provide a container image, a task execution role, and an infrastructure role, and ECS creates the rest of the web-facing stack for you.

I tried it on a tiny FastAPI service I use to expose home sensor data. The result was not magic, but it was honestly close enough.


The Real Value

What ECS Express Mode buys you is not “ECS but cheaper”. It is “ECS with much less ceremony”.

According to the ECS documentation, Express Mode creates and wires together:

  • an ECS service running on Fargate
  • an HTTPS Application Load Balancer
  • target groups and health checks
  • security groups
  • CloudWatch log groups
  • autoscaling policies
  • an ACM certificate
  • a public URL in the form https://<service-name>.ecs.<region>.on.aws/

That is a very solid default package for APIs, small dashboards, prototypes, and utilities that need a real endpoint but do not need a bespoke networking story on day one.


The Use Case

My example here is intentionally boring, because boring services are exactly where this kind of feature matters.

I have a small FastAPI app that reads sensor snapshots from DynamoDB and exposes them as JSON:

from fastapi import FastAPI
from typing import Any
import boto3
import os

app = FastAPI()

TABLE_NAME = os.getenv("SENSOR_TABLE", "home-sensors")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(TABLE_NAME)


@app.get("/sensors")
async def get_all_sensors() -> list[dict[str, Any]]:
    response = table.scan(Limit=100)
    return response.get("Items", [])


@app.get("/sensors/{sensor_id}")
async def get_sensor(sensor_id: str) -> dict[str, Any]:
    response = table.get_item(Key={"sensor_id": sensor_id})
    return response.get("Item", {})


@app.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}

Containerized, pushed to ECR, nothing fancy.

The old version of me would have opened CDK and started assembling the usual ECS/Fargate stack. The new version of me wanted to see how far the “three inputs only” promise actually goes.


First Deploy

The minimum CLI flow is smaller than I expected. AWS documents the create-express-gateway-service command with a required --primary-container, plus the execution and infrastructure roles:

aws ecs create-express-gateway-service \
  --primary-container '{"image":"123456789012.dkr.ecr.eu-west-1.amazonaws.com/home-sensors-api:latest"}' \
  --execution-role-arn arn:aws:iam::123456789012:role/ecsTaskExecutionRole \
  --infrastructure-role-arn arn:aws:iam::123456789012:role/ecsInfrastructureRoleForExpressServices \
  --monitor-resources

Out of the box, Express Mode defaults to:

  • the default ECS cluster
  • Fargate
  • 1 vCPU and 2 GB of memory
  • HTTPS on port 443
  • a generated service URL

For a throwaway web app that is already a huge amount of useful infrastructure.

One detail I like a lot: updates use canary deployments by default, not a crude “replace everything and hope”. AWS shifts a small percentage of traffic first, checks alarms and health checks, then completes the rollout.


Customizing the Service Without Falling Back to Full ECS

The nice surprise is that Express Mode still leaves room for a few meaningful knobs. You can set the service name, container port, health check path, environment variables, CPU, memory, and autoscaling targets without dropping into raw task definitions immediately.

This is the version I would actually keep:

aws ecs create-express-gateway-service \
  --service-name home-sensors-api \
  --execution-role-arn arn:aws:iam::123456789012:role/ecsTaskExecutionRole \
  --infrastructure-role-arn arn:aws:iam::123456789012:role/ecsInfrastructureRoleForExpressServices \
  --primary-container '{
    "image":"123456789012.dkr.ecr.eu-west-1.amazonaws.com/home-sensors-api:latest",
    "containerPort":8000,
    "environment":[
      {"name":"SENSOR_TABLE","value":"home-sensors-prod"},
      {"name":"LOG_LEVEL","value":"info"}
    ]
  }' \
  --health-check-path /health \
  --cpu 2 \
  --memory 4 \
  --scaling-target '{
    "minTaskCount":3,
    "maxTaskCount":10,
    "autoScalingMetric":"AVERAGE_CPU",
    "autoScalingTargetValue":60
  }' \
  --monitor-resources

That is still dramatically smaller than the equivalent amount of hand-written ECS plumbing.

For updates, the matching command is update-express-gateway-service, which can change the image, health check path, CPU, memory, environment variables, and scaling target.


What It Creates For You

The official docs are worth reading because they make the defaults explicit.

The most relevant ones for a small app are:

  • ALB sharing: up to 25 Express Mode services in the same VPC can share a load balancer
  • host-based routing: traffic is routed by host header
  • CloudWatch logs: enabled by default through the task definition
  • autoscaling: target tracking is configured automatically
  • ACM certificate: created for the generated HTTPS endpoint

That ALB sharing point matters more than it sounds. If you have a few hobby APIs or tiny internal apps, the “one ALB per service” pattern gets expensive quickly. Express Mode is opinionated, but in this case the opinion is a sensible one.


Where It Stops Being Enough

Express Mode is not meant to replace full ECS for every workload.

I would move down to plain ECS or CDK-managed ECS when I need private east-west service communication patterns I want to control explicitly, unusual networking requirements, a vanity domain with a curated ingress story, or custom deployment workflows outside the built-in canary defaults. And obviously if the infrastructure must live entirely inside an existing IaC stack, you will want something that fits that model properly.

That is not a criticism. It is exactly the boundary I would want.

Express Mode is best when the main problem is “I have a web container and I want it online properly”, not “I need total control over every ECS detail from minute one”.


Final Thoughts

This is one of those AWS features that does not feel revolutionary until you use it. Then you realize how much accidental work you have quietly normalized.

For the home sensor API, I did not need a bespoke ALB setup, custom listeners, or a hand-built scaling policy. I needed a correct, HTTPS-enabled, autoscaled container service with sane defaults. Express Mode gave me exactly that.

For personal projects and small-team services, that is a very good trade.


See also: