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:
Log Event ──► Log Stream ──► Log Group
(one line) (one source) (one app/service)| Unit | What It Is | Example |
|---|---|---|
| Log Event | A single timestamped line | 2026-08-15 10:00:01 [INFO] Invoice created |
| Log Stream | All events from one container run | ecs/payment-service/abc123task |
| Log Group | Bucket 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 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:
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/payment-service",
"awslogs-region": "ap-south-1",
"awslogs-stream-prefix": "ecs"
}
}| Option | Purpose |
|---|---|
awslogs-group | Which Log Group to write into |
awslogs-region | Which AWS region CloudWatch lives in |
awslogs-stream-prefix | Prefix 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:
{awslogs-stream-prefix}/{container-name}/{task-id}Example:
/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-0242ac120002Each 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:
{
"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:
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
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-CCC23 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.
[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 Def → logDriver: 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 lineWhat Happens During a Deployment (Task Replacement)
When ECS replaces tasks during a rolling deployment:
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-BBB1 ← STOPPED (stream stays, logs preserved)
├── ecs/payment-container/task-id-BBB2 ← active
├── ecs/payment-container/task-id-BBB3 ← active
└── ecs/payment-container/task-id-BBB4 ← NEW (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:
Crash scenario:
task-id-BBB2 crashes at 14:23:01
↓
ECS starts task-id-BBB5 at 14:23:07
↓
New 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:07To 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
{
"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"
}| Option | Notes |
|---|---|
awslogs-create-group: true | Auto-creates the Log Group if it doesn't exist. Safe for first deploys. |
executionRoleArn | Must have logs:* permissions. Separate from taskRoleArn. |
Querying Logs in CloudWatch Logs Insights
Basic Queries
-- All errors across all payment-service tasks
fields @timestamp, @message, @logStream
| filter @message like /ERROR/
| sort @timestamp desc
| limit 50-- Logs from a specific task only
fields @timestamp, @message
| filter @logStream = "ecs/payment-container/task-id-BBB2"
| sort @timestamp desc-- Count errors per task stream (which task is noisiest?)
stats count(*) as error_count by @logStream
| filter @message like /ERROR/
| sort error_count descCommon Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Missing IAM permissions | No logs appear in CloudWatch | Add logs:PutLogEvents to execution role |
| Wrong region in Task Def | Logs appear in wrong region | Match awslogs-region to your cluster region |
| Log Group not pre-created | Task fails to start | Set awslogs-create-group: true or pre-create the group |
| Querying wrong Log Group | Zero results | Verify Group name matches Task Def exactly |
| Container writing to file (not stdout) | No logs in CloudWatch | Redirect 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:
# Set 30-day retention on payment-service logs
aws logs put-retention-policy \
--log-group-name /ecs/payment-service \
--retention-in-days 30Common retention tiers:
| Use Case | Retention |
|---|---|
| Debug/dev | 7 days |
| Production apps | 30–90 days |
| Compliance/audit | 1–7 years |
Summary
┌─────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────┘