ECS service-to-service communication is one of those things that sounds straightforward โ containers talking to each other โ until you're staring at a connection refused error and have no idea why.
You've deployed your API and your worker service on ECS. They live in the same VPC. They should be able to talk. But there's no magic DNS that makes it work automatically. You need to tell ECS how to wire them together.
There are two ways to do that: Service Discovery (via AWS Cloud Map) and Service Connect (the newer, simpler option). Most guides dive straight into Terraform or CLI commands without explaining which one to pick or why. This is that explanation.
The core problem: containers have ephemeral IPs
When an ECS Fargate task starts, AWS assigns it a private IP from your VPC subnet. When it stops and a new one starts โ after a deploy, a crash, a scale event โ it gets a different IP.
You can't hardcode http://10.0.1.45:8080 in your API to talk to your worker. That IP will change. What you need is a stable name that always resolves to the IPs of currently-running, healthy tasks.
That's exactly what both Service Discovery and Service Connect provide. They just do it differently.
Service Discovery: DNS backed by Cloud Map
Service Discovery uses AWS Cloud Map to register your ECS tasks under a private DNS name. When a task starts, ECS registers its IP. When it stops, ECS deregisters it.
Your other services use a DNS name like worker.myapp.local to reach the worker. Cloud Map resolves that name to the IPs of healthy tasks. Your HTTP client picks one and connects directly.
How the DNS name is structured:
[service-name].[namespace]
For example: worker.production.local or api.internal
What Cloud Map actually does behind the scenes:
It creates a Route 53 private hosted zone for your namespace. For each running task, it creates an A record pointing to that task's private IP. When the task stops or fails its health check, the record is removed. You never touch Route 53 directly โ ECS and Cloud Map handle it.
The flow:
Service A calls "http://worker.production.local:3001"
โ DNS query hits Cloud Map
โ Cloud Map returns one or more task IPs
โ Service A connects directly to that IP
No load balancer in the middle. Direct container-to-container connection.
Service Connect: the simpler, newer approach
Service Connect was launched in 2022 and is now AWS's recommended approach for most ECS use cases. It's built on the same Cloud Map foundation but wraps it in a sidecar proxy that handles the complexity for you.
Instead of your application making raw DNS calls, ECS injects an Envoy proxy sidecar into each task. That proxy handles service discovery, load balancing, retries, and circuit breaking automatically. Your app just calls http://worker:3001 โ no namespace suffix, no DNS configuration in your code.
What Service Connect adds on top of Service Discovery:
- Short names: call
http://worker:3001instead ofhttp://worker.production.local:3001 - Cross-cluster communication: works across ECS clusters in the same AWS region, not just within one cluster
- Built-in metrics: CloudWatch gets request count, latency, and error rate per service pair automatically
- Load balancing at the proxy level: the sidecar distributes traffic across healthy tasks without your app needing to handle multiple IPs
The trade-off is a small resource overhead for the Envoy proxy โ typically 256 MB memory per task. For most startups, this is irrelevant. The operational simplicity is worth it.
Which one should you use?
Use Service Connect unless you have a specific reason not to.
Here's a quick decision guide:
| Situation | Use | |---|---| | Services in the same ECS cluster | Service Connect | | Services in different ECS clusters, same region | Service Connect | | gRPC services | Service Connect (supports HTTP/2 and gRPC) | | You want built-in traffic metrics | Service Connect | | You need cross-VPC communication (different VPCs) | Service Discovery + VPC peering | | You have non-ECS consumers (Lambda, EC2) that need to discover ECS services | Service Discovery | | You're using a service mesh (App Mesh) | Service Discovery |
For most startup architectures โ a handful of ECS services in one or two clusters โ Service Connect is the right answer. You don't need to configure Cloud Map namespaces manually, you don't need to think about DNS TTLs, and you get free observability.
Setting up Service Connect
Service Connect requires two things: a namespace (shared across services that need to talk) and port mapping with an appProtocol in your task definitions.
Step 1: Enable Service Connect on your ECS cluster
Every ECS cluster that uses Service Connect needs a default namespace. You can set this when creating the cluster or after the fact.
In the ECS console: go to your cluster โ Configuration โ Service Connect โ select a namespace (or create one).
With the AWS CLI:
aws ecs put-cluster-capacity-providers \
--cluster my-cluster \
--default-capacity-provider-strategy \
capacityProvider=FARGATE,weight=1,base=1
# Set the default namespace
aws ecs update-cluster \
--cluster my-cluster \
--service-connect-defaults namespace=myapp.local
Step 2: Add appProtocol to your task definition port mappings
This is the part most tutorials skip. Service Connect needs to know the protocol to generate the right metrics and handle routing correctly.
{
"containerDefinitions": [
{
"name": "worker",
"image": "your-ecr-image:latest",
"portMappings": [
{
"name": "worker-http",
"containerPort": 3001,
"hostPort": 3001,
"protocol": "tcp",
"appProtocol": "http"
}
]
}
]
}
Valid appProtocol values: http, http2, grpc. Use http for standard REST APIs.
Step 3: Enable Service Connect on the ECS service
When creating or updating the ECS service, turn on Service Connect and configure the endpoint.
In the console, under Service Connect:
- Turn on Service Connect: Client and server
- Port alias:
worker-http(matches the port mapping name) - Discovery name:
worker(what other services will call it) - DNS name:
worker(same as discovery name for simplicity) - Port:
3001
With the CLI:
aws ecs create-service \
--cluster my-cluster \
--service-name worker \
--task-definition worker:3 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-abc,subnet-def],securityGroups=[sg-worker],assignPublicIp=DISABLED}" \
--service-connect-configuration '{
"enabled": true,
"namespace": "myapp.local",
"services": [
{
"portName": "worker-http",
"discoveryName": "worker",
"clientAliases": [
{
"dnsName": "worker",
"port": 3001
}
]
}
]
}'
For the API service that calls the worker, enable Service Connect in client-only mode โ it doesn't expose its own endpoint to other services internally, but it can discover and call the worker:
aws ecs create-service \
--cluster my-cluster \
--service-name api \
--task-definition api:5 \
--desired-count 3 \
--launch-type FARGATE \
--network-configuration "..." \
--service-connect-configuration '{
"enabled": true,
"namespace": "myapp.local"
}'
Now your API containers can call http://worker:3001 and it just works. The Envoy sidecar handles DNS resolution, load balancing, and retries.
The security group gotcha (this is why it breaks)
The most common reason ECS service-to-service communication fails is a missing security group rule. This trips up everyone the first time.
With Fargate, each task gets its own elastic network interface (ENI). Traffic between tasks is subject to security group rules, even within the same VPC.
What you need:
The security group on the target service (the one being called) must allow inbound traffic from the security group on the calling service on the service's port.
API task security group (sg-api) โ calls โ Worker task (sg-worker, port 3001)
Required rule on sg-worker:
Type: Custom TCP
Port: 3001
Source: sg-api (the API service's security group)
Two ways to write this rule:
Option 1: Reference the calling security group (recommended)
aws ec2 authorize-security-group-ingress \
--group-id sg-worker \
--protocol tcp \
--port 3001 \
--source-group sg-api
This allows any task with the sg-api security group to call the worker on port 3001. It stays valid as task IPs change.
Option 2: Allow the entire VPC CIDR (easier but broader)
aws ec2 authorize-security-group-ingress \
--group-id sg-worker \
--protocol tcp \
--port 3001 \
--cidr 10.0.0.0/16
This allows anything in your VPC to call the worker. Fine for early-stage. Tighten it once you have more services.
The Envoy proxy port (Service Connect only):
With Service Connect, the Envoy proxy also listens on port 15001 (outbound) and 9901 (admin). You don't need to open these externally, but make sure you're not accidentally blocking them within the task's security group on loopback. In practice, this isn't usually an issue unless you've locked down egress rules very tightly.
Checking if service discovery is working
After deploying, here's how to verify:
From inside a task (exec into the container):
# ECS exec into a running task
aws ecs execute-command \
--cluster my-cluster \
--task <task-id> \
--container api \
--interactive \
--command "/bin/sh"
# Inside the container
curl http://worker:3001/health
# Should return 200 from your worker
# Check what's registered
nslookup worker.myapp.local
# Should return one or more task IPs
Check Cloud Map in the console:
Go to AWS Cloud Map โ Namespaces โ your namespace โ Services โ look at the registered instances. Each should have an IP matching a running Fargate task.
Check CloudWatch metrics (Service Connect only):
Go to CloudWatch โ Metrics โ ECS โ ServiceConnect. You'll see RequestCount, ResponseTime, and RequestCountFailures for each service-to-service connection. If requests are showing up here, Service Connect is working.
Service Discovery setup (if you need it)
If you need Service Discovery instead of Service Connect โ for example, to expose an ECS service to a Lambda function or to services in a different VPC โ here's the quick version.
Create a Cloud Map namespace:
aws servicediscovery create-private-dns-namespace \
--name myapp.local \
--vpc vpc-0abc123def456789
Create a Cloud Map service:
aws servicediscovery create-service \
--name worker \
--dns-config "NamespaceId=ns-xxxx,DnsRecords=[{Type=A,TTL=10}],RoutingPolicy=MULTIVALUE" \
--health-check-custom-config "FailureThreshold=1"
Link it to your ECS service:
aws ecs create-service \
--cluster my-cluster \
--service-name worker \
--task-definition worker:3 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "..." \
--service-registries "registryArn=arn:aws:servicediscovery:us-east-1:123456789:service/srv-xxxx"
Your calling service then uses worker.myapp.local to resolve to task IPs. Set DNS TTL to 10 seconds โ don't go higher, or you'll hold stale IPs after a deploy.
Note on DNS caching: Some runtimes cache DNS results longer than the TTL. Java does this by default. Node.js and Python generally respect TTLs. If you're seeing stale connections after a deploy, suspect DNS caching in your runtime before anything else.
Common mistakes
Calling the wrong hostname. With Service Connect, you use the short dnsName you configured (e.g., worker). With Service Discovery, you use the fully-qualified name (worker.myapp.local). Mixing these up is the #1 source of confusion.
Forgetting to add appProtocol to the task definition. Without it, Service Connect falls back to TCP mode and you lose HTTP metrics. It still works but you get less observability.
Not enabling Service Connect on client services. A service that only calls others (doesn't expose its own endpoint) still needs Service Connect enabled in client mode. Otherwise the Envoy proxy isn't injected and the short DNS name doesn't resolve.
Same namespace, different cluster. Services in different clusters can communicate via Service Connect, but both clusters must use the same Cloud Map namespace. Check your cluster's default namespace first.
What NoahOps does for you
NoahOps configures Service Connect automatically when you create a new ECS service. You pick which services need to talk to each other, we handle the namespace, port aliases, security group rules, and Envoy configuration. Noah AI lets you describe it in plain English โ "make my worker service reachable from my API" โ and generates the right configuration.
You own all of it in your AWS account. We just wire it up without you needing to read this post first.
Request a free demo at noahops.com.
FAQ
Can I use Service Connect and Service Discovery at the same time?
Yes. A single ECS service can have Service Connect enabled for internal cluster communication and also be registered in Cloud Map for external discovery. This is useful if you have Lambda functions that need to discover your ECS services while your ECS services also talk to each other.
Does Service Connect work with private ECR images?
Yes. Service Connect is about networking, not image pulling. Your tasks still pull from ECR through the VPC endpoint or NAT gateway as usual.
What happens during a deploy? Does traffic drop?
During a rolling deploy, new tasks start and register with Cloud Map / Service Connect before old tasks stop. The Envoy proxy handles in-flight connections to old tasks gracefully. Combined with a proper health check and minimumHealthyPercent: 100, you get zero-downtime deploys. See our guide on zero-downtime ECS deployments for the full configuration.
Does Service Connect work with ECS on EC2 (not Fargate)?
Yes, but the Envoy proxy sidecar must be added manually to your task definition. With Fargate, ECS injects it automatically. For most startups on Fargate, this isn't relevant.
How much does Service Connect cost?
There's no Service Connect-specific charge. You pay for the Envoy sidecar's resource consumption (it uses CPU/memory within your Fargate task) and the Cloud Map API calls (typically fractions of a cent). The CloudWatch metrics it generates count toward standard CloudWatch pricing.