devuplabs.cloud
Free previewConcept guide8 min read

Log Transmission

This page covers how logs flow from ECS containers to CloudWatch Log Groups, from the mechanics of the awslogs driver to a real-world cluster scenario with multiple services and tasks.

The Core Hierarchy

Before diving into transmission, anchor the mental model:

javascript
Log Event  ──►  Log Stream  ──►  Log Group
(one line)      (one source)     (one app/service)
UnitWhat It IsExample
Log EventA single timestamped line2026-08-15 10:00:01 [INFO] Invoice created
Log StreamAll events from one container runecs/payment-service/abc123task
Log GroupBucket for all streams of a service/ecs/payment-service

Rule of thumb

One Log Group per ECS Service. One Log Stream per Task (container instance).


The Transmission Pipeline

Your Container Writes to stdout/stderr

Your application code does nothing special:

python
# Python example
print("[INFO] Payment processed for invoice INV-9021")

Or in Java/Node/Go, any console.log, logger.info, fmt.Println that goes to stdout or stderr is captured.

Why stdout/stderr?

Containers are designed to be stateless. Writing logs to files inside a container is an anti-pattern, those files vanish when the container stops. stdout/stderr is the universal contract.


The ECS Agent Intercepts the Output

On every ECS host (or in Fargate's managed infra), the ECS Agent runs as a daemon. It:

  • Monitors the stdout/stderr streams of every running container
  • Reads the configured log driver from the Task Definition
  • Hands off log lines to the appropriate driver (in our case, awslogs)

The ECS Agent is the invisible middleman: your app never talks to CloudWatch directly.


The awslogs Log Driver Takes Over

The awslogs driver is a built-in Docker/ECS log driver that knows how to push log lines to CloudWatch. It reads three key config values from your Task Definition:

json
"logConfiguration": {
  "logDriver": "awslogs",
  "options": {
    "awslogs-group": "/ecs/payment-service",
    "awslogs-region": "ap-south-1",
    "awslogs-stream-prefix": "ecs"
  }
}
OptionPurpose
awslogs-groupWhich Log Group to write into
awslogs-regionWhich AWS region CloudWatch lives in
awslogs-stream-prefixPrefix for auto-generated stream names

CloudWatch Log Stream is Auto-Created

When the first log line arrives, the awslogs driver automatically creates a Log Stream inside the Log Group using this naming pattern:

javascript
{awslogs-stream-prefix}/{container-name}/{task-id}

Example:

javascript
/ecs/payment-service
  └── ecs/payment-container/3f8a1b2c-9d4e-11ed-a8fc-0242ac120002
  └── ecs/payment-container/7c2d9e1a-4b5f-11ed-a8fc-0242ac120002
  └── ecs/payment-container/aa91c3f0-6e7a-11ed-a8fc-0242ac120002

Each stream = one task run. Streams never mix between task instances.


IAM Permissions Authorize the Write

The ECS Task must have an IAM Task Execution Role with these CloudWatch permissions:

json
{
  "Effect": "Allow",
  "Action": [
    "logs:CreateLogGroup",
    "logs:CreateLogStream",
    "logs:PutLogEvents",
    "logs:DescribeLogStreams"
  ],
  "Resource": "arn:aws:logs:ap-south-1:123456789012:log-group:/ecs/*"
}

Without this, awslogs silently fails and logs disappear.


Buffering and Batching

The awslogs driver does not send every log line immediately. It:

  • Buffers log events in memory
  • Flushes to CloudWatch in batches (up to 1 MB or 10,000 events per PutLogEvents call)
  • Retries on transient network failures

This means there's typically a 1–5 second delay between your container logging something and it appearing in CloudWatch.


Real-World ECS Cluster Scenario

The Setup

Imagine a subscription billing platform (very Chargebee-flavoured). The ECS cluster has three services:

javascript
ECS Cluster: prod-billing-cluster
├── Service: api-gateway          (2 tasks running)
├── Service: payment-service      (3 tasks running)
└── Service: invoice-worker       (2 tasks running)

Total: 7 containers running simultaneously, all writing logs.


How CloudWatch Organizes This

javascript
CloudWatch Log Groups
├── /ecs/api-gateway
│     ├── ecs/api-container/task-id-AAA1
│     └── ecs/api-container/task-id-AAA2
│
├── /ecs/payment-service
│     ├── ecs/payment-container/task-id-BBB1
│     ├── ecs/payment-container/task-id-BBB2
│     └── ecs/payment-container/task-id-BBB3
│
└── /ecs/invoice-worker
      ├── ecs/worker-container/task-id-CCC1
      └── ecs/worker-container/task-id-CCC2

3 Log Groups, 7 Log Streams: one stream per running task.


Tracing a Log Line End-to-End

Let's trace a real event: Payment task BBB2 processes invoice INV-5523.

javascript
[1] payment-container (task BBB2)
    └── prints: "[INFO] Processing payment for INV-5523 | amount=₹4999"

[2] ECS Agent on that Fargate node
    └── intercepts stdout from BBB2
    └── reads Task DeflogDriver: awslogs, group: /ecs/payment-service

[3] awslogs driver
    └── buffers the log event
    └── calls PutLogEvents API to CloudWatch

[4] CloudWatch
    └── writes to Log Group: /ecs/payment-service
    └── writes to Log Stream: ecs/payment-container/task-id-BBB2

[5] You query CloudWatch Logs Insights:
    └── filter @logStream like /BBB2/ | filter @message like /INV-5523/
    └── Result: timestamp + full log line

What Happens During a Deployment (Task Replacement)

When ECS replaces tasks during a rolling deployment:

javascript
Before deployment:
  /ecs/payment-service
    ├── ecs/payment-container/task-id-BBB1  ← active
    ├── ecs/payment-container/task-id-BBB2  ← active
    └── ecs/payment-container/task-id-BBB3  ← active

During rolling deploy:
  ECS stops BBB1, starts BBB4

After deployment:
  /ecs/payment-service
    ├── ecs/payment-container/task-id-BBB1STOPPED (stream stays, logs preserved)
    ├── ecs/payment-container/task-id-BBB2  ← active
    ├── ecs/payment-container/task-id-BBB3  ← active
    └── ecs/payment-container/task-id-BBB4NEW (new stream auto-created)

Key insight

Old streams are never deleted by ECS. They persist for as long as the Log Group's retention policy allows. This is critical for post-mortem debugging after a bad deploy.


What Happens When a Container Crashes and Restarts

ECS Fargate auto-restarts failed containers as new tasks:

javascript
Crash scenario:
  task-id-BBB2 crashes at 14:23:01ECS starts task-id-BBB5 at 14:23:07New stream created: ecs/payment-container/task-id-BBB5

In CloudWatch:
  task-id-BBB2 stream ends abruptly at 14:23:01
  task-id-BBB5 stream starts at 14:23:07

To investigate the crash: check the last log events in BBB2's stream: that's where the error/stack trace will be.


Task Definition Configuration

Complete Example

json
{
  "family": "payment-service",
  "containerDefinitions": [
    {
      "name": "payment-container",
      "image": "123456789012.dkr.ecr.ap-south-1.amazonaws.com/payment-service:v2.1",
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/payment-service",
          "awslogs-region": "ap-south-1",
          "awslogs-stream-prefix": "ecs",
          "awslogs-create-group": "true"
        }
      }
    }
  ],
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
}
OptionNotes
awslogs-create-group: trueAuto-creates the Log Group if it doesn't exist. Safe for first deploys.
executionRoleArnMust have logs:* permissions. Separate from taskRoleArn.

Querying Logs in CloudWatch Logs Insights

Basic Queries

sql
-- All errors across all payment-service tasks
fields @timestamp, @message, @logStream
| filter @message like /ERROR/
| sort @timestamp desc
| limit 50
sql
-- Logs from a specific task only
fields @timestamp, @message
| filter @logStream = "ecs/payment-container/task-id-BBB2"
| sort @timestamp desc
sql
-- Count errors per task stream (which task is noisiest?)
stats count(*) as error_count by @logStream
| filter @message like /ERROR/
| sort error_count desc

Common Pitfalls

PitfallSymptomFix
Missing IAM permissionsNo logs appear in CloudWatchAdd logs:PutLogEvents to execution role
Wrong region in Task DefLogs appear in wrong regionMatch awslogs-region to your cluster region
Log Group not pre-createdTask fails to startSet awslogs-create-group: true or pre-create the group
Querying wrong Log GroupZero resultsVerify Group name matches Task Def exactly
Container writing to file (not stdout)No logs in CloudWatchRedirect app logs to stdout in your Dockerfile/app config

Retention Policy

By default, CloudWatch Log Groups never expire: logs accumulate indefinitely and you pay for storage forever. Set a retention policy per Log Group:

bash
# Set 30-day retention on payment-service logs
aws logs put-retention-policy \
  --log-group-name /ecs/payment-service \
  --retention-in-days 30

Common retention tiers:

Use CaseRetention
Debug/dev7 days
Production apps30–90 days
Compliance/audit1–7 years

Summary

javascript
┌─────────────────────────────────────────────────────────┐
│                    ECS Fargate Task                     │
│  ┌──────────────────────────────────────────────────┐   │
│  │  Container: payment-container                    │   │
│  │  └── app writes to stdout/stderr                 │   │
│  └────────────────────┬─────────────────────────────┘   │
│                       │ ECS Agent intercepts            │
│                       ▼                                 │
│  ┌──────────────────────────────────────────────────┐   │
│  │  awslogs Log Driver                              │   │
│  │  └── buffers events                              │   │
│  │  └── PutLogEvents API call (batched)             │   │
│  └────────────────────┬─────────────────────────────┘   │
└───────────────────────┼─────────────────────────────────┘
                        │ HTTPS to CloudWatch endpoint
                        ▼
┌─────────────────────────────────────────────────────────┐
│  CloudWatch Logs                                        │
│  Log Group: /ecs/payment-service                        │
│    └── Log Stream: ecs/payment-container/task-BBB1      │
│    └── Log Stream: ecs/payment-container/task-BBB2      │
│    └── Log Stream: ecs/payment-container/task-BBB3      │
└─────────────────────────────────────────────────────────┘

One Group per Service. One Stream per Task. One Event per log line.

Unlock all 24 AWS services & 291+ lab sessions (~180 hours)

Pricing