Back to Blog

AWS ECS Service Connect in 2026: Service-to-Service Communication Without Service Discovery

Use AWS ECS Service Connect for microservice communication: no service discovery setup, built-in load balancing, circuit breaking, Terraform configuration, and observability with CloudWatch.

Viprasol Tech Team
January 28, 2027
13 min read

AWS ECS Service Connect in 2026: Service-to-Service Communication Without Service Discovery

Before ECS Service Connect, connecting microservices in ECS required: Cloud Map namespaces, service discovery configurations, DNS TTL headaches, and custom health check logic. Service Connect, GA since late 2022 and mature in 2026, replaces all of that with a sidecar proxy that handles service-to-service routing automatically.

Services communicate using simple DNS names within the cluster (http://api-service:3000), Service Connect handles load balancing across task instances, and you get built-in circuit breaking and connection retries—without deploying a full service mesh like App Mesh.


Service Connect vs Alternatives

FeatureService ConnectCloud Map + DNSApp MeshALB
Setup complexityLowMediumHighLow
Service-to-service routing❌ (external only)
Circuit breaking✅ Built-in
mTLS
ObservabilityCloudWatchBasicX-Ray + CWALB logs
CostSidecar overheadCloud Map API callsComplex pricingALB hours
Best forSimple–medium microservicesLegacy ECS setupsEnterprise mTLSPublic endpoints

Use Service Connect when you have 2–10 internal services that need to call each other and you don't need mTLS. Upgrade to App Mesh only when you need mutual TLS or advanced traffic shaping.


Architecture

ECS Cluster (with Service Connect namespace)
├── api-service (port 3000)
│   └── Service Connect proxy sidecar (transparent)
├── auth-service (port 4000)
│   └── Service Connect proxy sidecar
├── notification-service (port 5000)
│   └── Service Connect proxy sidecar
└── worker-service (no ingress port needed)

# api-service calls auth-service:
fetch("http://auth-service:4000/validate")
# → intercepted by local proxy
# → routed to a healthy auth-service task instance
# → circuit breaker monitors failures

☁️ Is Your Cloud Costing Too Much?

Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.

  • AWS, GCP, Azure certified engineers
  • Infrastructure as Code (Terraform, CDK)
  • Docker, Kubernetes, GitHub Actions CI/CD
  • Typical audit recovers $500–$3,000/month in savings

Terraform Configuration

# terraform/ecs-service-connect.tf

# 1. Create the Service Connect namespace
resource "aws_service_discovery_http_namespace" "main" {
  name        = "${var.name}-${var.environment}"
  description = "Service Connect namespace for ${var.name} ${var.environment}"
  tags        = var.common_tags
}

# 2. ECS Cluster — enable Service Connect defaults
resource "aws_ecs_cluster" "main" {
  name = "${var.name}-${var.environment}"

  service_connect_defaults {
    namespace = aws_service_discovery_http_namespace.main.arn
  }

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  tags = var.common_tags
}

# 3. API Service — with Service Connect client + server config
resource "aws_ecs_service" "api" {
  name            = "${var.name}-${var.environment}-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_tasks.id]
    assign_public_ip = false
  }

  service_connect_configuration {
    enabled   = true
    namespace = aws_service_discovery_http_namespace.main.arn

    # This service IS discoverable by other services as "api-service"
    service {
      port_name      = "api"           # Must match container port_mappings name
      discovery_name = "api-service"  # DNS name other services use
      client_alias {
        port     = 3000
        dns_name = "api-service"       # http://api-service:3000
      }
    }

    # Logging for the Service Connect proxy sidecar
    log_configuration {
      log_driver = "awslogs"
      options = {
        awslogs-group         = "/ecs/${var.name}-${var.environment}/service-connect"
        awslogs-region        = var.aws_region
        awslogs-stream-prefix = "api"
      }
    }
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.api.arn
    container_name   = "api"
    container_port   = 3000
  }

  lifecycle {
    ignore_changes = [desired_count]
  }
}

# 4. Auth Service — internal only (no ALB)
resource "aws_ecs_service" "auth" {
  name            = "${var.name}-${var.environment}-auth"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.auth.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = var.private_subnet_ids
    security_groups = [aws_security_group.ecs_tasks.id]
    assign_public_ip = false
  }

  service_connect_configuration {
    enabled   = true
    namespace = aws_service_discovery_http_namespace.main.arn

    service {
      port_name      = "auth"
      discovery_name = "auth-service"
      client_alias {
        port     = 4000
        dns_name = "auth-service"
      }
    }
  }
  # No load_balancer block — internal service only
}

# 5. Worker Service — client only (calls other services but isn't called)
resource "aws_ecs_service" "worker" {
  name            = "${var.name}-${var.environment}-worker"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.worker.arn
  desired_count   = 1
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = var.private_subnet_ids
    security_groups = [aws_security_group.ecs_tasks.id]
    assign_public_ip = false
  }

  service_connect_configuration {
    enabled   = true
    namespace = aws_service_discovery_http_namespace.main.arn
    # No `service` block — this service is a client only, not a server
  }
}

Task Definition with Named Port Mappings

# terraform/task-definitions.tf

resource "aws_ecs_task_definition" "api" {
  family                   = "${var.name}-${var.environment}-api"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 512
  memory                   = 1024
  execution_role_arn       = aws_iam_role.ecs_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name      = "api"
      image     = "${aws_ecr_repository.api.repository_url}:${var.image_tag}"
      essential = true

      portMappings = [
        {
          name          = "api"      # MUST match service_connect_configuration service.port_name
          containerPort = 3000
          hostPort      = 3000
          protocol      = "tcp"
          appProtocol   = "http"   # Enables HTTP/2 in Service Connect proxy
        }
      ]

      environment = [
        { name = "NODE_ENV",          value = var.environment },
        # Service Connect makes these DNS names work automatically:
        { name = "AUTH_SERVICE_URL",  value = "http://auth-service:4000" },
        { name = "NOTIF_SERVICE_URL", value = "http://notification-service:5000" },
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = "/ecs/${var.name}-${var.environment}/api"
          awslogs-region        = var.aws_region
          awslogs-stream-prefix = "api"
        }
      }
    }
  ])
}

⚙️ DevOps Done Right — Zero Downtime, Full Automation

Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.

  • Staging + production environments with feature flags
  • Automated security scanning in the pipeline
  • Uptime monitoring + alerting + runbook automation
  • On-call support handover docs included

Service-to-Service Communication in Application Code

// lib/services/auth-client.ts
// Calls auth-service via Service Connect DNS — no SDK or discovery needed

interface ValidateTokenResponse {
  valid: boolean;
  userId?: string;
  teamId?: string;
  role?: string;
}

const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL ?? "http://auth-service:4000";

export async function validateToken(token: string): Promise<ValidateTokenResponse> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000); // 5s timeout

  try {
    const response = await fetch(`${AUTH_SERVICE_URL}/validate`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        // Internal service auth header (shared secret or JWT)
        "X-Internal-Key": process.env.INTERNAL_SERVICE_KEY!,
      },
      body: JSON.stringify({ token }),
      signal: controller.signal,
    });

    if (!response.ok) {
      if (response.status === 401) {
        return { valid: false };
      }
      throw new Error(`Auth service error: ${response.status}`);
    }

    return response.json();
  } catch (err) {
    if (err instanceof Error && err.name === "AbortError") {
      throw new Error("Auth service timeout");
    }
    throw err;
  } finally {
    clearTimeout(timeout);
  }
}

// The Service Connect proxy handles:
// - Load balancing across auth-service task instances
// - Health checks (unhealthy tasks stop receiving traffic)
// - Connection retries (configurable)
// - Circuit breaking (configurable threshold)
// - Metrics emission to CloudWatch

Observability

Service Connect automatically emits metrics to CloudWatch under the AWS/ECS/ManagedScaling namespace. Key metrics to monitor:

# CloudWatch alarms for Service Connect health
resource "aws_cloudwatch_metric_alarm" "auth_service_5xx" {
  alarm_name          = "${var.name}-auth-service-5xx-rate"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "HTTPCode_Target_5XX_Count"
  namespace           = "AWS/ECS"
  period              = 60
  statistic           = "Sum"
  threshold           = 10
  alarm_description   = "auth-service returning >10 5xx errors per minute"
  treat_missing_data  = "notBreaching"

  dimensions = {
    ServiceName = "${var.name}-${var.environment}-auth"
    ClusterName = aws_ecs_cluster.main.name
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
}

# Request latency alarm
resource "aws_cloudwatch_metric_alarm" "auth_service_latency" {
  alarm_name          = "${var.name}-auth-service-p99-latency"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 3
  extended_statistic  = "p99"
  metric_name         = "TargetResponseTime"
  namespace           = "AWS/ECS"
  period              = 60
  threshold           = 0.5  # 500ms p99
  alarm_description   = "auth-service p99 latency > 500ms"

  dimensions = {
    ServiceName = "${var.name}-${var.environment}-auth"
    ClusterName = aws_ecs_cluster.main.name
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
}

Security Group Configuration

# Internal services communicate on their container ports
resource "aws_security_group" "ecs_tasks" {
  name        = "${var.name}-${var.environment}-ecs-tasks"
  vpc_id      = var.vpc_id
  description = "ECS tasks — allow internal service communication"

  # Allow all traffic within the security group (service-to-service)
  ingress {
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    self        = true  # Other tasks in the same SG
    description = "Internal service-to-service via Service Connect"
  }

  # Allow inbound from ALB for public-facing services
  ingress {
    from_port       = 3000
    to_port         = 3000
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
    description     = "ALB to api-service"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = var.common_tags
}

Cost Estimates

ComponentCost
Service Connect proxy sidecarIncluded in Fargate task CPU/memory
Cloud Map HTTP namespace$0.10/1K API calls (usually < $5/month)
CloudWatch metrics$0.30/metric/month (10 services ≈ $10/month)
Additional Fargate CPU for sidecar~64 CPU units / task (minimal)
Total vs ALB for internalALB: $16/month fixed + $0.008/LCU vs Service Connect: ~$10–$15/month in metrics

See Also


Working With Viprasol

We design and deploy ECS microservice architectures with Service Connect for B2B SaaS products — from simple 2-service setups through complex 10+ service platforms. Our cloud team has migrated multiple products from ALB-based internal routing to Service Connect with zero-downtime deployments.

What we deliver:

  • Service Connect namespace and cluster Terraform modules
  • Task definition templates with named port mappings
  • Internal service client libraries with timeout and retry
  • CloudWatch alarms for per-service error rate and latency
  • Security group rules for internal service communication

See our cloud infrastructure services or contact us to design your ECS microservice networking.

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need DevOps & Cloud Expertise?

Scale your infrastructure with confidence. AWS, GCP, Azure certified team.

Free consultation • No commitment • Response within 24 hours

Viprasol · Big Data & Analytics

Making sense of your data at scale?

Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.