You tick a todo as done · ECS on Fargate + RDS MySQL

10 steps from your browser to the database and back. Then a background job finishes the work.

InternetAWS Region · us-east-1VPCPublic subnet · to IGWPrivate subnet · to NATYour browseron the internetAPI GatewayWebSocket API · live lineCloudWatch Logsapp log linesInternet gatewaythe VPC's doorFargate taskruns the app containerNAT gatewayone-way exitRDS MySQLdatabase: todoioENIworker's network cardWorker Lambdaupdates monthly statsSQS queuetodo-eventsDead-letter queuetodo-events-dlqThe live line is already open1You tick a todo as done2Into the VPC3Through the firewall4Who are you?5Ask the database6The database answers7Drop a job in the queue8Back to you9Leave a note10The worker picks it up11Save the result12Tell your browser13It shows up instantly14Job done15If the worker keeps failing16

Text version of this diagram.

What sits where:

  • AWS Region · us-east-1: A Region is a part of the world where AWS has data centers. Most things you build live in one Region. us-east-1 is in northern Virginia. contains:
    • VPC: Virtual private cloud: your own private network inside AWS. Nothing gets in or out except the ways you allow. contains:
      • Public subnet · to IGW: A slice of the VPC that can reach the internet directly, through the internet gateway (IGW). contains:
        • Fargate task (runs the app container): Your app packed as a container, run by AWS on servers you never see. A task is one running copy.
        • NAT gateway (one-way exit): Lets things in a private subnet start connections out to the internet, while nothing outside can start one in.
      • Private subnet · to NAT: A slice of the VPC with no door to the internet. Things here can reach out through the NAT gateway, but nothing outside can reach in. contains:
        • RDS MySQL (database: todoio): A MySQL database that AWS runs for you, including the server, patches and backups. todoio is the database's name.
        • ENI (worker's network card): Elastic network interface: a virtual network card. It gives the worker an address inside your VPC.
      • Internet gateway (the VPC's door): The door between a VPC and the internet. Without one, nothing in the VPC can talk to the internet directly.
    • API Gateway (WebSocket API · live line): The front door for the app's web requests. A WebSocket API keeps a connection open, so the server can push updates to your browser instantly.
    • CloudWatch Logs (app log lines): Collects the lines your app writes about what it's doing (its logs), so you can search them later.
    • Worker Lambda (updates monthly stats): A second Lambda function that does slow work in the background, taking jobs off the queue.
    • SQS queue (todo-events): Simple Queue Service: a waiting line for messages. One part of the app drops a job in; another picks it up later.
    • Dead-letter queue (todo-events-dlq): A dead-letter queue: where a message goes after failing several times, so someone can look at it instead of it being retried forever.
  • Your browser (on the internet)

Steps, in order:

  1. The live line is already open. Path: Your browser → API Gateway. When you logged in, your browser opened a WebSocket to API Gateway. It stays open, so AWS can send you messages any time.
  2. You tick a todo as done. Path: Your browser. Your browser sends PATCH /todos/42 over HTTPS.
  3. Into the VPC. Path: Your browser → Internet gateway. todo.io points at the task's public IP, so the request comes in through the VPC's internet gateway. A Fargate task gets a new public IP every time it restarts, so the DNS record has to follow it. In Level 2 todo.io points at a load balancer's DNS name instead, which never changes.
  4. Through the firewall. Path: Internet gateway → Fargate task. The security group on the task's own network card allows HTTPS on port 443, so the request reaches the container. Checks: security-group: todo-app-sg: TCP 443 from 0.0.0.0/0 → allow.
  5. Who are you?. Path: Fargate task. The app checks your signed session cookie and finds your user id, u-123. This is the app's own login, not AWS.
  6. Ask the database. Path: Fargate task → RDS MySQL. The app sends SQL to RDS MySQL in the private subnet on port 3306. It logs in with a database username and password, not with IAM. Checks: security-group: todo-db-sg: TCP 3306 from todo-app-sg → allow; mysql: MySQL user todo_app: UPDATE on todoio.todos → allow.
  7. The database answers. Path: RDS MySQL → Fargate task. MySQL says: 1 row changed.
  8. Drop a job in the queue. Path: Fargate task → Internet gateway → SQS queue. The app sends a message to an SQS queue saying a todo was completed, instead of doing the slow work now. Checks: iam: todo-app-task-role: sqs:SendMessage on todo-events → allow.
  9. Back to you. Path: Fargate task → Internet gateway → Your browser. You get 200 OK, and the todo shows a tick.
  10. Leave a note. Path: Fargate task → Internet gateway → CloudWatch Logs. The app's log line goes to CloudWatch Logs so Todo.io's engineers can see what happened. Like SQS, it's a public AWS service, so it leaves through the internet gateway.
  11. The worker picks it up. Path: SQS queue → Worker Lambda. Lambda keeps polling the queue for you and runs the worker function with the new message. Checks: iam: todo-worker-role: sqs:ReceiveMessage, sqs:DeleteMessage → allow.
  12. Save the result. Path: Worker Lambda → ENI → RDS MySQL. The worker adds 1 to your monthly stats in RDS MySQL. It's attached to the VPC too, with its own ENI in the private subnet. Checks: security-group: todo-db-sg: TCP 3306 from todo-worker-sg → allow; mysql: MySQL user todo_worker: INSERT, UPDATE on todoio.user_stats → allow.
  13. Tell your browser. Path: Worker Lambda → ENI → NAT gateway → Internet gateway → API Gateway. The worker looks up your connection id in the database and posts a message to it through the WebSocket API. It's attached to the VPC with no public IP, so it gets out through a NAT gateway and the internet gateway. Checks: iam: todo-worker-role: execute-api:ManageConnections → allow.
  14. It shows up instantly. Path: API Gateway → Your browser. Your "12 done this month" badge updates on screen, with no refresh.
  15. Job done. Path: Worker Lambda → SQS queue. The worker returns without an error, so the Lambda service deletes the message from the queue. Your code never deletes it.
  16. If the worker keeps failing. Path: SQS queue → Dead-letter queue. If the worker fails on the same message 3 times, SQS moves it to a dead-letter queue. It isn't lost, and it stops being retried forever.
Step 1 of 16
Step 1 of 16
  1. When you logged in, your browser opened a WebSocket to API Gateway. It stays open, so AWS can send you messages any time.

    Show me the real thing

    The WebSocket handshake

    GET /prod HTTP/1.1
    Host: ws123.execute-api.us-east-1.amazonaws.com
    Upgrade: websocket
    Connection: Upgrade
    
    → API Gateway assigns connection id Ab3xYzEXAMPLE=

    Exam tipAn API Gateway WebSocket API has $connect, $disconnect and $default routes. Each one can call Lambda or an HTTP backend.

  2. Your browser sends PATCH /todos/42 over HTTPS.

    Show me the real thing

    The HTTP request

    PATCH /todos/42 HTTP/1.1
    Host: todo.io
    Cookie: session=eyJ1c2VySWQiOiJ1LTEyMyJ9.Zm9v
    Content-Type: application/json
    
    {"done":true}
  3. todo.io points at the task's public IP, so the request comes in through the VPC's internet gateway. A Fargate task gets a new public IP every time it restarts, so the DNS record has to follow it. In Level 2 todo.io points at a load balancer's DNS name instead, which never changes.

    Exam tipA subnet is "public" only because its route table sends 0.0.0.0/0 to an internet gateway. Nothing else makes it public.

  4. The security group on the task's own network card allows HTTPS on port 443, so the request reaches the container.

    • 🛡Security group todo-app-sg: TCP 443 from 0.0.0.0/0Allowed
    Show me the real thing

    Inbound rules on todo-app-sg

    Type    Protocol  Port  Source
    HTTPS   TCP       443   0.0.0.0/0

    Exam tipThe classic way to do HTTPS is a free AWS Certificate Manager (ACM) certificate on a load balancer or CloudFront (Level 2). Since 2025 ACM can also sell you an exportable certificate to install on the instance or container itself.

  5. The app checks your signed session cookie and finds your user id, u-123. This is the app's own login, not AWS.

  6. The app sends SQL to RDS MySQL in the private subnet on port 3306. It logs in with a database username and password, not with IAM.

    • 🛡Security group todo-db-sg: TCP 3306 from todo-app-sgAllowed
    • 🗄MySQL MySQL user todo_app: UPDATE on todoio.todosAllowed
    Show me the real thing

    The SQL (real code sends the values as parameters, never pasted into the string)

    UPDATE todos SET done = true WHERE id = 42 AND user_id = 'u-123';

    Exam tipA security group rule can use another security group as its source: "anything wearing todo-app-sg may connect". Exam favourite.

  7. MySQL says: 1 row changed.

  8. The app sends a message to an SQS queue saying a todo was completed, instead of doing the slow work now.

    • 🔑IAM todo-app-task-role: sqs:SendMessage on todo-eventsAllowed
    Show me the real thing

    SendMessage request

    {
      "QueueUrl": "https://sqs.us-east-1.amazonaws.com/111122223333/todo-events",
      "MessageBody": "{\"type\":\"todo.completed\",\"userId\":\"u-123\",\"todoId\":\"42\"}"
    }

    Exam tipA queue decouples the app from the worker. If the worker is slow or broken, messages simply wait (up to 14 days).

  9. You get 200 OK, and the todo shows a tick.

    Show me the real thing

    The HTTP response

    HTTP/1.1 200 OK

    Exam tipSecurity groups are stateful: a reply to an allowed request goes back out automatically. No outbound rule needed for it.

  10. The app's log line goes to CloudWatch Logs so Todo.io's engineers can see what happened. Like SQS, it's a public AWS service, so it leaves through the internet gateway.

    Show me the real thing

    The log line

    2026-10-11T18:42:07Z INFO PATCH /todos/42 user=u-123 status=200 14ms

    Exam tipFargate ships container output with the awslogs log driver, using the task execution role.

  11. Lambda keeps polling the queue for you and runs the worker function with the new message.

    • 🔑IAM todo-worker-role: sqs:ReceiveMessage, sqs:DeleteMessageAllowed
    Show me the real thing

    The event the worker receives (trimmed)

    {
      "Records": [
        {
          "messageId": "19dd0b57-b21e-4ac1",
          "eventSource": "aws:sqs",
          "eventSourceARN": "arn:aws:sqs:us-east-1:111122223333:todo-events",
          "body": "{\"type\":\"todo.completed\",\"userId\":\"u-123\",\"todoId\":\"42\"}"
        }
      ]
    }

    Exam tipSQS never pushes. Lambda polls it for you (an "event source mapping"). Set the queue's visibility timeout to at least 6× the function's timeout.

  12. The worker adds 1 to your monthly stats in RDS MySQL. It's attached to the VPC too, with its own ENI in the private subnet.

    • 🛡Security group todo-db-sg: TCP 3306 from todo-worker-sgAllowed
    • 🗄MySQL MySQL user todo_worker: INSERT, UPDATE on todoio.user_statsAllowed
    Show me the real thing

    The SQL

    INSERT INTO user_stats (user_id, month, completed) VALUES ('u-123', '2026-10', 1)
      ON DUPLICATE KEY UPDATE completed = completed + 1;

    Exam tipStandard queues deliver at least once, so the same message can arrive twice and count your todo twice. Real workers are idempotent: they record each message id and skip repeats.

  13. The worker looks up your connection id in the database and posts a message to it through the WebSocket API. It's attached to the VPC with no public IP, so it gets out through a NAT gateway and the internet gateway.

    • 🔑IAM todo-worker-role: execute-api:ManageConnectionsAllowed
    Show me the real thing

    Posting to a connection

    POST /prod/@connections/Ab3xYzEXAMPLE= HTTP/1.1
    Host: ws123.execute-api.us-east-1.amazonaws.com
    
    {"type":"stats.updated","month":"2026-10","created":5,"completed":12}

    Exam tipA Lambda attached to a VPC needs a NAT gateway (or a VPC endpoint) to reach anything outside the VPC, even AWS services like SQS.

  14. Your "12 done this month" badge updates on screen, with no refresh.

  15. The worker returns without an error, so the Lambda service deletes the message from the queue. Your code never deletes it.

    Exam tipIf the worker throws, the message isn't deleted. It becomes visible again after the visibility timeout and is retried.

  16. If the worker fails on the same message 3 times, SQS moves it to a dead-letter queue. It isn't lost, and it stops being retried forever.

    Show me the real thing

    Redrive policy on todo-events

    {
      "deadLetterTargetArn": "arn:aws:sqs:us-east-1:111122223333:todo-events-dlq",
      "maxReceiveCount": 3
    }

    Exam tipA dead-letter queue must be the same type as its source queue: a FIFO queue needs a FIFO DLQ.

What to notice

  • AWS runs the server and you ship a container. One task in one Availability Zone is still a single point of failure (Level 2 fixes that).
  • IAM never checked your SQL. MySQL has its own users; IAM only manages the database server itself.
  • The database sits in a private subnet with no route to the internet. Only the security groups you allowed can reach it.
  • The database password lives in the app config here. In production it belongs in Secrets Manager (Level 1).
  • You got your answer before the job ran. The queue lets slow work happen later without making you wait.
  • If the worker breaks, messages wait in the queue, and after 3 failures they land in the DLQ. Nothing is lost.
  • A WebSocket stays open, so the server side can send you updates without you asking.
  • A Lambda in a VPC can't reach public AWS services by itself, so this VPC needed a NAT gateway.
  • You, the customer, were never an IAM user. IAM controls the app, not its customers.