Learning/AWS SQS/01 — Messaging Fundamentals
Beginner 30 min read

Messaging Fundamentals

No AWS in this module. Before a queue can be useful, the problem it solves has to be real to you — so this module starts from an ordinary synchronous system, breaks it four times, and derives the queue as the answer.

If you read only one module before an interview, read this one. It is where the reasoning lives, and every later module is a detail inside it.

1. What you will learn

  • Explain, without naming any product, what problem a message queue solves
  • Describe four distinct failure modes of synchronous service-to-service calls
  • Define producer, consumer, message, queue, polling and acknowledgement precisely
  • Distinguish push from pull delivery, and say why SQS chose pull
  • Distinguish a queue from a log, and a queue from a database table

2. Start with a system that works

An online store. A customer places an order.

flowchart LR
    C["🧑 Customer"] --> A["Order Service"]
    A -->|"HTTP POST /charge<br/>(waits for reply)"| B["Payment Service"]
    B --> DB[("Payments DB")]
    B -.->|"200 OK"| A
    A -.->|"200 OK — order placed"| C

Reading the diagram. The customer waits for the Order Service. The Order Service waits for the Payment Service. The Payment Service waits for its database. Every arrow is a synchronous call: the caller holds a thread open and does nothing until the answer arrives.

This design is not stupid. It is the correct first design. It is simple, the customer learns the outcome immediately, and when everything is fast and healthy it is perfect.

Now let us break it.

3. Four ways this breaks

Failure 1 — Payment is slow

The payment provider's p99 drifts from 200 ms to 8 seconds. Nothing is down.

What the customer sees: an 8-second spinner on checkout.

What is less obvious: the Order Service is holding a thread for those 8 seconds. With a 200-thread pool and 8-second calls, the Order Service can serve 25 requests per second, total. It was serving 800. The Order Service has not failed, but it has become as slow as its slowest dependency.

📌 Remember this. In a synchronous chain, your latency is the sum of everything downstream, and your throughput is capped by your slowest dependency.

Failure 2 — Payment is down

The payment provider returns connection refused for four minutes.

Every order placed in those four minutes fails. Not "is delayed" — fails. The customer sees an error, and the order does not exist anywhere. The work is gone, and the only record that it was ever attempted is a line in a log file.

The Order Service is healthy. Its database is healthy. Yet from the customer's point of view the store is down, because a dependency two hops away is.

Failure 3 — A traffic spike

A marketing email goes out. Traffic goes from 500 orders/minute to 5,000 orders/minute for about ninety seconds.

The Payment Service can handle 800/minute. It now receives 5,000. It does not gracefully serve 800 and politely decline the rest — it accepts all 5,000, its connection pool saturates, its latency climbs, its own timeouts start firing, and it collapses. The Order Service, still holding threads, collapses with it.

The demand was temporary. The outage was not. Total orders that ninety seconds needed was well within the day's capacity — it just arrived in the wrong shape.

Failure 4 — A deploy

Payment Service is deployed. Rolling restart, thirty seconds.

Every in-flight request during those thirty seconds fails. Routine maintenance became customer-facing errors, so deploys move to 2 a.m., so they happen less often, so each one is bigger and riskier.

flowchart TB
    subgraph FM["The same call, four ways it fails"]
        direction LR
        A["Order Service"] --> B["Payment Service"]
    end

    F1["🐌 <b>Slow</b><br/>Caller's threads block<br/>Caller becomes as slow<br/>as its dependency"]
    F2["💥 <b>Down</b><br/>Work is lost, not delayed<br/>Failure propagates<br/>to the customer"]
    F3["📈 <b>Spike</b><br/>No buffer<br/>Temporary demand<br/>causes lasting outage"]
    F4["🚀 <b>Deploy</b><br/>In-flight requests fail<br/>Routine work becomes<br/>customer-visible"]

    FM --- F1
    FM --- F2
    FM --- F3
    FM --- F4

What these four have in common

They are not four problems. They are one problem wearing four hats:

The producer's fate is bound to the consumer's fate, at the instant of the call.

Payment being slow, absent, overloaded or restarting are all the same thing from the Order Service's side: the consumer is not ready right now. And because the call is synchronous, "not ready right now" propagates straight through to the customer.

4. Why the obvious fixes do not fix it

Before reaching for a queue, it is worth seeing why the things you would naturally try first each trade one problem for another. Interviewers ask about this, and "we already tried X" is a much stronger answer than "we use a queue because queues are good".

FixWhat it helpsWhat it costs
TimeoutsCaller stops blocking foreverThe work is now definitely lost instead of possibly slow. Failure 1 becomes failure 2
RetriesSurvives a brief blipAmplifies load precisely when the downstream is struggling. Turns failure 1 into failure 3 (Module 10)
Bigger thread poolMore concurrent waitsMemory, context switching, and more simultaneous pressure on the struggling dependency
Circuit breakerStops hammering a dead serviceFails fast — which is correct, and still means the order is rejected. Failure 2 remains
BulkheadsContains the blast radiusPayment requests still fail; you have only stopped them taking search down with them

Every one of these is a good idea and you should use them. None of them addresses the root issue, because none of them changes the binding: the work still has to happen now, or not at all.

Why can't retries alone solve this? A retry keeps the work alive only as long as the caller is alive and willing to wait. It is an in-memory buffer of size one, with a lifetime measured in seconds, that vanishes if the caller restarts. What you actually need is durable storage of intent.

5. The insight

Look again at what the Order Service is really trying to express:

"A payment needs to happen for order 4471."

That is a fact about the world. It stays true whether or not the Payment Service is available right now. The synchronous design conflates two separate things:

  1. Recording that the work must happen — fast, local, always possible
  2. Doing the work — slow, dependent on others, sometimes impossible right now

Separate them, and put something durable in between.

flowchart LR
    C["🧑 Customer"] --> A["Order Service"]
    A -->|"1. store intent"| Q[("📥 Queue")]
    Q -.->|"2. ack"| A
    A -.->|"202 Accepted<br/>(~20 ms)"| C
    Q ==>|"3. when ready"| B["Payment Service"]
    B --> DB[("Payments DB")]

Reading the diagram. The solid arrows on the left happen in about twenty milliseconds and involve nobody but the Order Service and the queue. The heavy arrow on the right happens whenever the Payment Service is ready — which might be 50 ms later, or four minutes later if it is down.

Term: a queue is durable, ordered-ish storage of messages, where producers add and consumers remove.

Now replay the four failures.

FailureBeforeAfter
Payment slowCustomer waits 8 s; Order Service throughput collapsesCustomer waits 20 ms; the queue grows; payments finish a bit later
Payment downOrder fails; work lostOrder accepted; messages accumulate; processed on recovery
Traffic spikePayment collapses; cascadeQueue absorbs the burst; payment runs flat out and catches up
DeployIn-flight requests failMessages wait; the new version picks them up

Every one of the four becomes a queue that is temporarily longer than usual.

📌 Remember this — the single most important sentence in this module. A queue converts failure into delay. That is almost always a better trade. It is not always an acceptable one, and §9 covers when it is not.

6. The vocabulary, precisely

Now that the picture exists, the words can be defined against it.

MessageSimple: a piece of data one program sends to another. Technical: an immutable unit of data placed on a queue, consisting of a body and optional metadata, which exists independently of both sender and receiver. Example: {"orderId": "4471", "amountCents": 2999, "currency": "GBP"}

QueueSimple: a durable waiting area for messages. Technical: a store that accepts messages from producers and hands them to consumers, retaining each until it is consumed or expires. Example: sqs-course-payments

ProducerSimple: whatever puts messages in. Technical: any process calling the queue's send operation. Example: the Order Service.

ConsumerSimple: whatever takes messages out. Technical: any process that retrieves and processes messages, then acknowledges them. Example: the Payment Service.

PollingSimple: the consumer repeatedly asking "anything for me?" Technical: the consumer initiating a request to the queue for available messages, rather than the queue initiating delivery. Example: a loop calling ReceiveMessage every few hundred milliseconds — or better, a long-polling call that waits (Module 12).

AcknowledgementSimple: the consumer telling the queue "done, you can forget this one." Technical: an explicit signal, separate from receipt, after which the queue permanently removes the message. Until it arrives the queue assumes the work may still need doing. Example: in SQS this is a DeleteMessage call (Module 04).

Note the shape of that last one carefully. Receiving and acknowledging are separate events. If they were the same event, a consumer crash between "got it" and "did it" would lose the message — which is exactly failure 2 all over again, just moved. This separation is the entire reason Module 08 exists.

Why must acknowledgement be separate from receipt? Because the queue has no way to observe whether your processing succeeded. Only your code knows. Making acknowledgement explicit is how the queue delegates that knowledge to the one party that has it.

7. Three properties worth naming

Decoupling

Producer and consumer no longer need to know about each other. Three distinct kinds, and interviews reward telling them apart:

KindMeaning
TemporalThey need not be running at the same time
SpatialNeither needs the other's address, port or health
FailureOne being down does not make the other fail

Load levelling

The queue absorbs the difference between how fast work arrives and how fast it is done.

Concretely: arrivals spike to 5,000/min for 90 seconds while the consumer sustains 800/min.

arrived during spike   = 5000 × 1.5 = 7,500
processed during spike =  800 × 1.5 = 1,200
backlog at peak        = 6,300 messages

drain rate after spike = 800 − 500 (normal arrivals) = 300/min
time to drain          = 6,300 ÷ 300 = 21 minutes

Nothing failed. The last message of the spike is processed 21 minutes late. Whether that is fine depends entirely on the business — and now it is a number you can decide about rather than an outage. Module 15 makes this arithmetic rigorous.

📌 Load levelling converts a throughput problem into a latency problem. That is the trade, and it is worth saying out loud in a design review.

Backpressure — and the trap

Backpressure is a slow consumer's ability to make a fast producer slow down.

Here is the thing people get wrong: an unbounded queue does not give you backpressure. It removes it. A synchronous call at least made the producer wait. The queue lets the producer run at full speed into a buffer that grows forever, so the system feels fine right up until the backlog is so long that the oldest messages are worthless — or, in SQS's case, silently deleted at the retention limit (Module 05).

flowchart LR
    subgraph NOBP["❌ Unbounded — overload is hidden"]
        P1["Producer<br/>5,000/min"] --> Q1[("Queue<br/>grows forever")] --> C1["Consumer<br/>800/min"]
    end
    subgraph BP["✅ Bounded — overload is signalled"]
        P2["Producer<br/>throttled"] -->|"rejected when full"| Q2[("Queue<br/>capped")] --> C2["Consumer<br/>800/min"]
    end

Real backpressure needs something to watch the queue and react: reject requests when the backlog crosses a threshold, or autoscale consumers, or shed low-priority work. The queue gives you the time to react; it does not react for you.

Why doesn't an unbounded queue give backpressure? Because backpressure is a signal travelling backwards, from consumer to producer. An unbounded buffer's entire purpose is to absorb that signal so the producer never feels it. Absorbing it is useful for bursts and dangerous for sustained overload — because the two look identical until it is too late.

8. Push versus pull

Who initiates delivery?

flowchart TB
    subgraph PUSH["Push — the broker decides"]
        direction LR
        QB[("Queue")] -->|"sends whether or not<br/>the consumer can cope"| CB["Consumer"]
    end
    subgraph PULL["Pull — the consumer decides"]
        direction LR
        CC["Consumer"] -->|"asks when it has capacity"| QC[("Queue")]
        QC -.->|"responds"| CC
    end
PushPull
LatencyLower — delivery is immediateSlightly higher — bounded by poll frequency
OverloadBroker can swamp a slow consumerConsumer never receives more than it asked for
Broker complexityMust track consumer addresses, health, capacityStateless with respect to consumers
Adding a consumerBroker must learn about itIt just starts asking

SQS is pull. Consumers poll; SQS never opens a connection to your code.

Why pull, when push sounds more efficient? Three reasons, and the third is the deep one. First, a pull consumer can never be overwhelmed — it asks for exactly what it can handle, which is real backpressure at the delivery layer. Second, SQS needs no knowledge of your consumers, so you can scale from 1 to 1,000 of them with no registration step. Third, push requires the broker to track liveness, and liveness in a distributed system is unknowable — which is precisely the problem Module 08 shows SQS solving with a timer instead.

⚠️ Common mistake. "Lambda triggers make SQS push-based." They do not. The Lambda service runs pollers that call ReceiveMessage on your behalf (Module 19). SQS still pushes nothing. The word "trigger" is doing a lot of misleading work.

9. What a queue costs you

An honest module has to include this section. Asynchrony is a trade, not a free upgrade.

Your API contract changes

Before:  200 OK  { "orderId": "4471", "status": "PAID" }
After:   202 Accepted  { "orderId": "4471", "status": "PENDING",
                         "statusUrl": "/orders/4471" }

202 Accepted means "I have durably recorded that this should happen." It does not mean it happened. Somebody now has to answer "did it work?" — a status endpoint, a webhook, a push notification, or a UI that tolerates pending states. That is product work, not just engineering work, and it is the part that gets forgotten in design reviews.

Per-request latency usually goes up

The synchronous path was Order → Payment → DB. The async path is Order → Queue → (wait to be polled) → Payment → DB. You have added a network hop and a polling interval.

📌 Asynchrony improves availability and throughput stability. It does not improve latency. A candidate who says "we used a queue to make it faster" is describing a different benefit than the one they achieved.

Debugging gets harder

There is no stack trace spanning producer and consumer. The two halves are separate processes, separate logs, possibly separate teams. Answering "what happened to order 4471?" requires a correlation ID threaded through the message and logged on both sides — which is why Module 17 treats it as mandatory rather than nice to have.

Exactly-once stops being available

Once a message can be redelivered, your consumer must be safe to run twice. This is not an SQS quirk; it is inherent to durable messaging, and Module 09 is devoted to it.

When the trade is wrong

Converting failure into delay is the wrong move when the caller genuinely needs the answer:

  • Authentication. "You are logged in, probably, eventually" is not a product.
  • Fraud/risk checks that gate the transaction. The decision must precede the action.
  • Reads. A queue is for work to be done, not for questions to be answered.
  • Anything where a stale result is dangerous — a price quote, an inventory reservation at the moment of purchase.

🎯 Interview point. Being able to say "I would not put a queue here, because…" is worth more than any amount of enthusiasm about decoupling.

10. Queue versus log versus table

Three storage shapes that beginners conflate. The distinction predicts almost everything in Module 27.

Queue (SQS)Log (Kafka)Table (Postgres)
ConsumptionDestructive — read and it is goneNon-destructive — it staysNon-destructive
PositionThe queue tracks what is outstandingConsumers track their own offsetQuery whatever you like
Replay✗ Impossible✓ Seek to any offset✓ Query again
Two independent readers✗ They compete for messages✓ Independent consumer groups✓ Trivially
RetentionShort (SQS: 14 days max)Long or unlimitedUntil deleted
Query✗ None✗ By offset only✓ Arbitrary
Scaling shapeAdd consumers freelyBounded by partition countBounded by the database

The one-line versions:

  • A queue answers "what work is outstanding?" — and forgets each item once done.
  • A log answers "what happened, in order?" — and remembers.
  • A table answers "what is true right now?"

⚠️ Common mistake. Using a database table as a queue. It works at small scale and then does not: polling SELECT ... FOR UPDATE SKIP LOCKED turns your database into the bottleneck for a workload that exists precisely to take load off it, and you now own the visibility-timeout and retry logic yourself. (This is a legitimate pattern — it is how the outbox in Module 24 works — but as a relay into a real queue, not as the queue.)

11. Step-by-step: the life of one message, conceptually

Before any AWS API appears, here is the shape Module 05 will make precise:

  1. The producer decides work needs doing and builds a message describing it.
  2. The producer sends it to the queue and receives an acknowledgement that it is stored.
  3. The producer returns to its caller — the work is accepted, not done.
  4. The queue holds the message durably. It may wait milliseconds or hours.
  5. A consumer, on its own schedule, polls for work.
  6. The queue hands over the message and — critically — does not yet delete it.
  7. The consumer processes.
  8. The consumer acknowledges. Only now does the queue discard the message.
  9. If the consumer crashes between 6 and 8, the queue eventually hands the message to someone else.

Step 9 is why duplicates exist. Step 6 is why they are possible. Hold on to both.

12. Real-world example

An e-commerce checkout, with numbers.

Synchronous version. Checkout calls payment inline. The provider's p99 is 8 seconds.

Customer-visible p99   : 8,200 ms
Order Service threads  : 200
Max sustained checkouts: 200 ÷ 8 s ≈ 25/sec
Behaviour when payment is down : checkout returns 500; order lost
Behaviour on a 10× spike       : thread pool exhausts; checkout unavailable

Asynchronous version. Checkout validates, writes the order, enqueues a payment message, returns 202.

Customer-visible p99   : 80 ms          (validate + insert + enqueue)
Order Service threads  : 200
Max sustained checkouts: 200 ÷ 0.08 s = 2,500/sec
Behaviour when payment is down : orders accepted; queue grows; drains on recovery
Behaviour on a 10× spike       : queue absorbs it; payments run flat out and catch up

A 100× improvement in checkout capacity, from the same hardware, by declining to wait.

What it cost them. The order page now shows "Payment processing" for a few seconds. A support runbook for "customer says they were charged but the order says pending". A GET /orders/{id} endpoint that did not previously exist. And an idempotency guard in the payment worker, because a worker crash after charging and before acknowledging would otherwise charge twice (Module 09).

That is the honest ledger. The trade is clearly worth it here — and being able to state both sides is what distinguishes a design decision from a fashion.

13. Common mistakes

Treating a queue as a fix for a permanently slow consumer. Why it's wrong: if the consumer's sustained throughput is below the sustained arrival rate, the backlog grows without bound and messages eventually expire. A queue absorbs bursts, not deficits. Instead: fix the consumer's throughput, or shed load. The queue buys you time to do one of those.

Using a queue for request/response. Why it's wrong: you have rebuilt RPC with worse latency, no timeout semantics and no backpressure. Instead: call synchronously, or design a genuinely asynchronous flow with a status resource.

Assuming async means faster. Why it's wrong: per-request latency goes up. The wins are availability, throughput stability and failure isolation. Instead: say which of those three you are buying.

Forgetting that somebody has to answer "did it work?" Why it's wrong: 202 Accepted moves a product question into the backlog, where it gets forgotten until support tickets arrive. Instead: design the status mechanism at the same time as the queue.

Putting the queue on the read path. Why it's wrong: queues carry work to be done, not questions to be answered. Requests will pile up behind unrelated work. Instead: read synchronously from a datastore.

14. Interview questions

🟢 Beginner

What is a message queue? What are a producer and a consumer? A durable store that holds messages between the program that creates them (producer) and the program that processes them (consumer), so the two need not run at the same time.

What does asynchronous processing mean? The caller gets an acknowledgement that the work was accepted rather than completed, and the work happens later on someone else's schedule.

🟡 Intermediate

What is the difference between push and pull delivery? Which does SQS use and why? Push: the broker initiates delivery, so latency is lower but a slow consumer can be swamped and the broker must track consumer health. Pull: the consumer initiates, so it never receives more than it asked for and the broker is stateless about consumers. SQS is pull — which is what lets you scale to any number of consumers with no registration, and means SQS never has to determine whether your consumer is alive.

What is load levelling? Give an example with numbers. The queue absorbing the difference between arrival rate and service rate. At 5,000/min arriving against 800/min served for 90 seconds, you accumulate 6,300 messages and drain them over about 21 minutes at a 300/min surplus. Nothing fails; the last message is 21 minutes late.

Why must acknowledgement be separate from receipt? Because only the consumer knows whether processing succeeded. If receipt implied acknowledgement, every consumer crash would silently lose a message.

🔴 Advanced

A queue converts a failure into a delay. When is that the wrong trade?

Reasoning: the trade is wrong whenever the caller's next action depends on the outcome. Three classes:

  1. Gating decisions — authentication, authorisation, fraud checks, inventory reservation at the moment of purchase. Proceeding without the answer is not a degraded experience, it is an incorrect one.
  2. Time-sensitive validity — a price quote or a rate-limited token whose value decays. Delivering it late is equivalent to not delivering it.
  3. Reads. A queue is a work distribution mechanism; a read has no work to distribute.

There is also a subtler case: when the delay is unbounded in practice. If the consumer's sustained capacity is below the sustained arrival rate, "delay" is a euphemism for "loss at the retention limit" — which is failure 2 with extra steps and worse observability.

Why doesn't an unbounded queue give you backpressure? Backpressure is a signal propagating backwards from consumer to producer. An unbounded buffer's function is to absorb exactly that signal. This is desirable for bursts and dangerous for sustained overload, and the two are indistinguishable from the producer's side until the backlog is already unrecoverable. Real backpressure requires something watching the depth and acting — rejecting, throttling, scaling or shedding.

⚫ System design

Design the checkout flow for a retailer expecting a 10× Black Friday spike. Where do queues go, and what breaks if you put one in the wrong place?

A good answer separates the path into stages and asks, per stage, does the customer need the answer now?

  • Synchronous, necessarily: authentication, cart validation, inventory reservation, fraud screening. These gate the transaction. Queueing them produces oversells and fraud losses.
  • Asynchronous, safely: payment capture (if authorisation is synchronous), order fulfilment, confirmation email, warehouse notification, analytics, loyalty points, recommendation updates.

Then the failure analysis:

  • Wrong placement 1: a queue in front of inventory reservation. Two customers both "succeed" on the last item; you discover the conflict minutes later and cancel one order after taking payment.
  • Wrong placement 2: a queue on the read path, e.g. fetching cart contents. Reads now wait behind unrelated writes.
  • Right placement, wrong capacity: queueing fulfilment without scaling the consumers. The queue absorbs the spike and then takes eleven hours to drain, so orders placed at 9 a.m. ship on Saturday. The system never failed and the customer experience did.

The last point is the one that distinguishes a strong answer: a queue moves the problem from availability to latency, and you must then explicitly own the latency. State the drain time, and scale consumers (Module 14) so it is acceptable.

15. Summary

  • Synchronous coupling means the producer's fate is bound to the consumer's, at the instant of the call. Slow, down, overloaded and deploying are all the same failure from the caller's side.
  • Timeouts, retries, bigger pools and circuit breakers each trade one failure for another. None changes the binding, because the work must still happen now.
  • A queue separates recording that work must happen from doing it, and stores the intent durably in between.
  • It converts failure into delay. Usually a great trade; explicitly not always.
  • It buys temporal, spatial and failure decoupling, plus load levelling.
  • It costs you inline results (202 Accepted), higher per-request latency, harder debugging, and the permanent need for idempotency.
  • An unbounded queue removes backpressure rather than providing it. Something must watch the depth and react.
  • Pull delivery is what lets consumers protect themselves, and is why SQS consumers poll.
  • Queue ≠ log ≠ table. Consumption is destructive, there is no replay, and two consumers on one queue compete rather than both receiving.

← Previous: 00 — Prerequisites · Index: Course home · Next: 02 — Introduction to SQS