<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maz I</title>
    <description>The latest articles on DEV Community by Maz I (@codewithmaz).</description>
    <link>https://dev.to/codewithmaz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4069321%2F1791ed1f-1a10-4123-8273-387a0ea02340.png</url>
      <title>DEV Community: Maz I</title>
      <link>https://dev.to/codewithmaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codewithmaz"/>
    <language>en</language>
    <item>
      <title>Designing a Production Serverless API: Beyond API Gateway + Lambda</title>
      <dc:creator>Maz I</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/codewithmaz/designing-a-production-serverless-api-beyond-api-gateway-lambda-1d9c</link>
      <guid>https://dev.to/codewithmaz/designing-a-production-serverless-api-beyond-api-gateway-lambda-1d9c</guid>
      <description>&lt;h1&gt;
  
  
  Designing a Production Serverless API: Beyond API Gateway + Lambda
&lt;/h1&gt;

&lt;p&gt;A serverless API can begin with a diagram that looks almost too easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   ↓
API Gateway
   ↓
Lambda
   ↓
DynamoDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for many applications, this is a perfectly reasonable starting point.&lt;/p&gt;

&lt;p&gt;But now imagine we are building an order API.&lt;/p&gt;

&lt;p&gt;Traffic is unpredictable. Most of the day we receive only a few requests, but during promotions thousands of customers can arrive within a short period.&lt;/p&gt;

&lt;p&gt;A customer sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /orders
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"P10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our requirements initially sound straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the order.&lt;/li&gt;
&lt;li&gt;Check inventory.&lt;/li&gt;
&lt;li&gt;Take payment.&lt;/li&gt;
&lt;li&gt;Send a confirmation email.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We could put everything inside one Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reserveInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;chargePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;Until it doesn't.&lt;/p&gt;

&lt;p&gt;This article isn't about memorizing AWS services.&lt;/p&gt;

&lt;p&gt;Instead, we'll start with that simple implementation and make the requirements progressively harder.&lt;/p&gt;

&lt;p&gt;Every time the design breaks, we'll ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What problem do we need to solve next?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is how our simple Lambda gradually becomes a production serverless architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why serverless in the first place?
&lt;/h2&gt;

&lt;p&gt;Our traffic is unpredictable.&lt;/p&gt;

&lt;p&gt;Running servers continuously for occasional spikes could mean maintaining capacity that sits mostly unused.&lt;/p&gt;

&lt;p&gt;Lambda is attractive because AWS manages the underlying execution infrastructure and can increase concurrent execution as demand grows, within the applicable scaling and account limits.&lt;/p&gt;

&lt;p&gt;So our first version is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer
   ↓
API Gateway
   ↓
CreateOrder Lambda
   ↓
DynamoDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DynamoDB also fits the serverless model well for workloads where its access-pattern-oriented data model makes sense.&lt;/p&gt;

&lt;p&gt;But selecting serverless doesn't remove architecture.&lt;/p&gt;

&lt;p&gt;We are about to discover why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #1: One Lambda is doing too much
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Save order        ✅
Reserve inventory ✅
Charge payment    ✅
Send email        ❌ timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our Lambda throws an error.&lt;/p&gt;

&lt;p&gt;From the client's perspective, the request failed.&lt;/p&gt;

&lt;p&gt;So the client retries.&lt;/p&gt;

&lt;p&gt;What should happen?&lt;/p&gt;

&lt;p&gt;Should we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create another order?&lt;/li&gt;
&lt;li&gt;reserve inventory again?&lt;/li&gt;
&lt;li&gt;charge the customer again?&lt;/li&gt;
&lt;li&gt;attempt the email again?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have mixed operations with very different failure characteristics inside one request.&lt;/p&gt;

&lt;p&gt;Sending an email should not determine whether an already successful payment is considered successful.&lt;/p&gt;

&lt;p&gt;So our first architectural improvement is &lt;strong&gt;decoupling&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email doesn't belong in the critical request path
&lt;/h2&gt;

&lt;p&gt;After payment succeeds, we don't need the customer to wait while an email provider processes a message.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment succeeds
      ↓
     SQS
      ↓
Email Lambda
      ↓
Email provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the payment workflow can finish while email processing happens independently.&lt;/p&gt;

&lt;p&gt;Amazon SQS acts as a buffer between the producer and consumer.&lt;/p&gt;

&lt;p&gt;This gives us an important principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A queue does not make the consumer faster. It allows producers and consumers to operate at different rates.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also isolates failures.&lt;/p&gt;

&lt;p&gt;If the email provider is temporarily unavailable, that does not need to break order creation.&lt;/p&gt;

&lt;p&gt;But asynchronous systems introduce a new issue.&lt;/p&gt;

&lt;p&gt;Lambda processing from SQS can result in messages being processed more than once, so AWS recommends making processing idempotent.&lt;/p&gt;

&lt;p&gt;That brings us to one of the most important concepts in serverless systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #2: Retries can duplicate business operations
&lt;/h2&gt;

&lt;p&gt;Imagine the customer sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server creates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORD-9001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the response is lost because of a network timeout.&lt;/p&gt;

&lt;p&gt;The customer retries.&lt;/p&gt;

&lt;p&gt;Without protection, we might create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORD-9002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for exactly the same checkout.&lt;/p&gt;

&lt;p&gt;Worse, the same problem can happen during payment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment provider charges €100  ✅
             ↓
Lambda loses connection        ❌
             ↓
Lambda retries
             ↓
Charge another €100?           😬
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A normal database rollback cannot solve this.&lt;/p&gt;

&lt;p&gt;The payment provider exists outside our database transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotency
&lt;/h3&gt;

&lt;p&gt;An operation is &lt;strong&gt;idempotent&lt;/strong&gt; when repeating the same logical request does not create an additional side effect.&lt;/p&gt;

&lt;p&gt;For order creation, the client might send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Idempotency-Key: checkout-abc-123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checkout-abc-123
      ↓
Create ORD-9001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that exact request is retried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checkout-abc-123
      ↓
Already processed
      ↓
Return ORD-9001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create ORD-9002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For payment, we want the same concept.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment idempotency key = ORD-9001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a network failure causes us to retry, the retry represents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Give me the result of the same payment operation.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Perform a brand-new payment.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This gives us a useful mental rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Retry makes systems resilient. Idempotency makes retries safe.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Problem #3: We now have a workflow
&lt;/h2&gt;

&lt;p&gt;Our order process has grown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create order
     ↓
Check inventory
     ↓
Reserve inventory
     ↓
Process payment
     ↓
Update order
     ↓
Queue email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could manually call Lambda A from Lambda B, Lambda B from Lambda C and keep adding flags to DynamoDB.&lt;/p&gt;

&lt;p&gt;But now we have questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if payment times out?&lt;/li&gt;
&lt;li&gt;What if the card is declined?&lt;/li&gt;
&lt;li&gt;Which errors should retry?&lt;/li&gt;
&lt;li&gt;How long should we wait?&lt;/li&gt;
&lt;li&gt;What happens after the last retry?&lt;/li&gt;
&lt;li&gt;How do we know which step the order is currently in?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point we are no longer dealing with a simple chain of functions.&lt;/p&gt;

&lt;p&gt;We are dealing with a &lt;strong&gt;workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is where AWS Step Functions becomes useful.&lt;/p&gt;

&lt;p&gt;Step Functions lets us model workflows as &lt;strong&gt;state machines&lt;/strong&gt; and orchestrate distributed applications and microservices.&lt;/p&gt;

&lt;p&gt;Our order process can now be represented explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Order
     ↓
Reserve Inventory
     ↓
Process Payment
    /     \
   /       \
Success   Failure
  ↓          ↓
Update     Handle
Order      failure
  ↓
Queue Email
  ↓
Complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step Functions without the mystery
&lt;/h2&gt;

&lt;p&gt;A few states are enough to understand most of our example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Task
&lt;/h3&gt;

&lt;p&gt;Do something.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ReserveInventory
ProcessPayment
UpdateOrder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Choice
&lt;/h3&gt;

&lt;p&gt;Make a decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Payment result
               /        \
              /          \
         SUCCESS        DECLINED
            ↓              ↓
        Continue       Failure path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Choice state adds conditional branching to a Step Functions workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry
&lt;/h3&gt;

&lt;p&gt;Try a failed operation again.&lt;/p&gt;

&lt;p&gt;But only when retrying makes sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catch
&lt;/h3&gt;

&lt;p&gt;If the operation cannot recover, route execution to another state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Succeed / Fail
&lt;/h3&gt;

&lt;p&gt;End the workflow intentionally as successful or failed.&lt;/p&gt;

&lt;p&gt;The important part is not memorizing state names.&lt;/p&gt;

&lt;p&gt;It's understanding that &lt;strong&gt;business flow is now visible outside our Lambda implementation&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical failure is not business failure
&lt;/h2&gt;

&lt;p&gt;Consider payment.&lt;/p&gt;

&lt;p&gt;A provider timeout might mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;temporary network problem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is potentially retryable.&lt;/p&gt;

&lt;p&gt;But:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CARD_DECLINED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not a temporary infrastructure failure.&lt;/p&gt;

&lt;p&gt;Retrying the same declined card three times probably makes no sense.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment
   |
   +-- TIMEOUT
   |      ↓
   |    Retry
   |
   +-- CARD_DECLINED
          ↓
       Do not retry
          ↓
       Ask customer
       for another method
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step Functions supports error-specific retry and catch behavior.&lt;/p&gt;

&lt;p&gt;This gives us another useful distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Temporary technical failure
→ Retry

Valid negative business result
→ Business workflow path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every undesirable result should become an exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is a DLQ?
&lt;/h2&gt;

&lt;p&gt;Retry/Catch and a Dead-Letter Queue solve related but different problems.&lt;/p&gt;

&lt;p&gt;Imagine SQS contains a message that repeatedly fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message
  ↓
attempt 1 ❌
attempt 2 ❌
attempt 3 ❌
attempt 4 ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of allowing it to continually interfere with normal processing, it can eventually be moved to a &lt;strong&gt;dead-letter queue&lt;/strong&gt; for investigation or later redrive.&lt;/p&gt;

&lt;p&gt;So remember:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retry
→ Try this operation again.

Catch
→ The operation did not recover.
  Which workflow path should run?

DLQ
→ This queued message repeatedly failed.
  Move it away from normal processing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They are not interchangeable concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #4: We don't have one giant rollback anymore
&lt;/h2&gt;

&lt;p&gt;Now suppose the workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Order       ✅
Reserve Inventory  ✅
Charge Payment     ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this were one relational database transaction, we might think:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But inventory, payment and shipping may be completely separate services.&lt;/p&gt;

&lt;p&gt;There is no global database transaction covering everything.&lt;/p&gt;

&lt;p&gt;So if payment permanently fails, we must undo the earlier successful business action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Release inventory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leads us to the &lt;strong&gt;Saga pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saga in simple language
&lt;/h2&gt;

&lt;p&gt;A Saga breaks a distributed transaction into smaller local transactions.&lt;/p&gt;

&lt;p&gt;For important forward actions, we define an appropriate &lt;strong&gt;compensating action&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forward action          Compensation

Create order        →   Cancel order
Reserve inventory   →   Release inventory
Charge payment      →   Refund payment
Create shipment     →   Cancel shipment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Order       ✅
Reserve Inventory  ✅
Charge Payment     ✅
Create Shipment    ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We might compensate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shipment failed
      ↓
Refund payment
      ↓
Release inventory
      ↓
Cancel order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A compensation isn't necessarily a magical reversal of history.&lt;/p&gt;

&lt;p&gt;A refund, for example, is another business operation that compensates for a successful charge.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;There are two common Saga coordination styles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choreography
&lt;/h3&gt;

&lt;p&gt;Services react to events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
     ↓
Inventory service

InventoryReserved
     ↓
Payment service

PaymentFailed
     ↓
Inventory service releases stock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Orchestration
&lt;/h3&gt;

&lt;p&gt;A central workflow explicitly coordinates the steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            Step Functions
                 |
        +--------+--------+
        |        |        |
     Order   Inventory  Payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our example is a natural fit for orchestration because the workflow has clear business states, retries and compensations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #5: Some work can happen in parallel
&lt;/h2&gt;

&lt;p&gt;Payment has succeeded.&lt;/p&gt;

&lt;p&gt;Now we need to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send confirmation email
Update loyalty points
Notify warehouse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These operations do not necessarily depend on one another.&lt;/p&gt;

&lt;p&gt;Running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email
  ↓
Loyalty
  ↓
Warehouse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;would unnecessarily serialize them.&lt;/p&gt;

&lt;p&gt;Step Functions has a &lt;strong&gt;Parallel state&lt;/strong&gt; for independent branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Payment Success
                    ↓
                 Parallel
              /      |       \
             ↓       ↓        ↓
          Email   Loyalty   Warehouse
              \      |       /
               \     |      /
                 complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Parallel state starts its branches concurrently and waits for all branches to terminate before moving on.&lt;/p&gt;

&lt;p&gt;But this introduces an architectural choice.&lt;/p&gt;

&lt;p&gt;If the main workflow &lt;strong&gt;must know that all three operations completed&lt;/strong&gt;, Parallel makes sense.&lt;/p&gt;

&lt;p&gt;If these are independent side effects that the main workflow doesn't need to wait for, event-driven fan-out may be cleaner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            PaymentSucceeded
                  ↓
              Event/Event Bus
             /       |       \
            ↓        ↓        ↓
         Email    Loyalty   Warehouse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Parallel state means “do these things concurrently and my workflow cares about their completion.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;while event choreography often means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“These consumers can react independently and the producer doesn't need to coordinate them.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Map: parallel processing for collections
&lt;/h2&gt;

&lt;p&gt;Parallel is useful when the branches perform different jobs.&lt;/p&gt;

&lt;p&gt;What if an order contains 500 items and we need to check every item?&lt;/p&gt;

&lt;p&gt;That's the same operation repeated over a collection.&lt;/p&gt;

&lt;p&gt;A Step Functions &lt;strong&gt;Map state&lt;/strong&gt; fits that problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 order items
       ↓
      Map
       ↓
Check inventory for each item
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large workloads, Step Functions also provides Distributed Map; AWS recommends that mode for scenarios such as concurrency above 40 iterations or very large execution histories/datasets.&lt;/p&gt;

&lt;p&gt;But this raises another question.&lt;/p&gt;

&lt;p&gt;Should all 500 checks execute simultaneously?&lt;/p&gt;

&lt;p&gt;Maybe not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #6: Scaling can become dangerous
&lt;/h2&gt;

&lt;p&gt;This is one of the most important serverless lessons.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 inventory checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the inventory dependency can safely handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 concurrent operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running all 500 at once would simply move the bottleneck downstream.&lt;/p&gt;

&lt;p&gt;The exact same principle appears in ordinary Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hugeArray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processItem&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can create too much concurrency.&lt;/p&gt;

&lt;p&gt;Different technology, same engineering rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Parallelism improves throughput until the dependency becomes the bottleneck.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now apply that to Lambda itself.&lt;/p&gt;

&lt;p&gt;Suppose our API normally receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20 requests/second
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but a promotion suddenly sends thousands.&lt;/p&gt;

&lt;p&gt;Lambda can scale concurrency.&lt;/p&gt;

&lt;p&gt;That sounds excellent until every execution calls a payment provider that safely supports only 100 requests per second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thousands of requests
        ↓
      Lambda
  scales aggressively
        ↓
Payment Provider
     🔥🔥🔥
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lambda tier can scale faster than the dependency behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reserved concurrency
&lt;/h3&gt;

&lt;p&gt;Lambda supports &lt;strong&gt;reserved concurrency&lt;/strong&gt;, which can reserve concurrency for a function while also placing an upper bound on that function's concurrency. AWS specifically notes it can help prevent overwhelming downstream resources such as database connections.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment Lambda
maximum concurrency = 100
          ↓
Payment Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now scaling has a guardrail.&lt;/p&gt;

&lt;p&gt;But simply throttling thousands of requests isn't always a good customer experience.&lt;/p&gt;

&lt;p&gt;For asynchronous workloads, SQS can absorb the burst:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5,000 jobs
     ↓
    SQS
====================
 jobs waiting safely
====================
     ↓
Lambda consumers
controlled throughput
     ↓
Dependency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQS/Lambda event source mappings also support concurrency controls.&lt;/p&gt;

&lt;p&gt;Again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Automatic scaling is not automatically safe architecture.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Always ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can Lambda scale?

        ↓ yes

Can everything behind Lambda
scale at the same rate?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cold starts and provisioned concurrency
&lt;/h2&gt;

&lt;p&gt;When Lambda needs a new execution environment, initialization occurs before the handler processes the request.&lt;/p&gt;

&lt;p&gt;That extra initialization latency is commonly called a &lt;strong&gt;cold start&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A simplified comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warm

Request
  ↓
Existing environment
  ↓
handler()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cold

Request
  ↓
Initialize environment
  ↓
Load runtime/code/dependencies
  ↓
handler()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also why reusable clients are often initialized outside the handler when appropriate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// reuse client&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For latency-sensitive workloads, Lambda also offers &lt;strong&gt;provisioned concurrency&lt;/strong&gt;, which keeps execution environments initialized ahead of demand.&lt;/p&gt;

&lt;p&gt;Don't confuse the two concepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reserved concurrency
→ controls/reserves concurrency capacity

Provisioned concurrency
→ keeps execution environments initialized
  for predictable startup latency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem #7: DynamoDB scales, but our key design still matters
&lt;/h2&gt;

&lt;p&gt;A serverless database does not remove data-modeling decisions.&lt;/p&gt;

&lt;p&gt;DynamoDB uses partition-key values to determine how items are distributed internally. Items sharing a partition-key value are grouped together, and when a sort key exists, they are ordered by sort-key value.&lt;/p&gt;

&lt;p&gt;Suppose we choose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK = merchantId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usually traffic is distributed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant A → moderate traffic
Merchant B → moderate traffic
Merchant C → moderate traffic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But during a huge campaign:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant A → enormous traffic 🔥
Merchant B → small
Merchant C → small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now one partition-key value becomes disproportionately busy.&lt;/p&gt;

&lt;p&gt;AWS recommends designing partition keys with many distinct values and reasonably uniform activity.&lt;/p&gt;

&lt;p&gt;This is why DynamoDB design begins with &lt;strong&gt;access patterns&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partition key and sort key, simplified
&lt;/h3&gt;

&lt;p&gt;A useful mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Partition Key
→ Which logical group does this item belong to?

Sort Key
→ How do I identify/order items inside that group?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK = MERCHANT#M123

SK = ORDER#2026-08-10#9001
SK = ORDER#2026-08-11#9002
SK = ORDER#2026-08-12#9003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PK = MERCHANT#M123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can retrieve that merchant's order collection.&lt;/p&gt;

&lt;p&gt;The timestamp in the sort key can help support chronological/range-oriented access.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSI: another access path
&lt;/h3&gt;

&lt;p&gt;Suppose our base table is optimized for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get an order by orderId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but another requirement is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get orders by merchant and date
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;Global Secondary Index (GSI)&lt;/strong&gt; can use a different partition and sort key from the base table.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base table

PK = ORDER#9001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GSI

PK = MERCHANT#M123
SK = 2026-08-10#ORDER#9001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same underlying item.&lt;/p&gt;

&lt;p&gt;Different access path.&lt;/p&gt;

&lt;p&gt;The key lesson is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DynamoDB schema design is strongly driven by how the application needs to retrieve data.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a different mental model from designing relational tables first and later adding indexes to optimize queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eventual consistency: correct doesn't always mean immediately visible everywhere
&lt;/h2&gt;

&lt;p&gt;Suppose payment succeeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORDER#9001
status = PAID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediately afterward, another component reads through a GSI.&lt;/p&gt;

&lt;p&gt;Could it briefly observe older data?&lt;/p&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;DynamoDB table/LSI reads can use eventual or strong consistency, while GSI reads are eventually consistent.&lt;/p&gt;

&lt;p&gt;Simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write:
status = PAID
      ↓
immediate eventual read
      ↓
possibly PENDING briefly
      ↓
later read
      ↓
PAID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't mean the write disappeared.&lt;/p&gt;

&lt;p&gt;It reflects the consistency guarantee of that read path.&lt;/p&gt;

&lt;p&gt;For an immediately authoritative order confirmation, we may prefer reading the exact order from the base table using the appropriate consistency requirement instead of assuming every secondary view has already converged.&lt;/p&gt;

&lt;p&gt;Another distributed-systems lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Correct state and immediate visibility of that state everywhere are different guarantees.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Problem #8: Every Lambda should not have permission to everything
&lt;/h2&gt;

&lt;p&gt;Our architecture now contains several functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreateOrder Lambda
Inventory Lambda
Payment Lambda
Email Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The easiest IAM policy would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It would also be a terrible production boundary.&lt;/p&gt;

&lt;p&gt;A better architecture uses &lt;strong&gt;least privilege&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreateOrder Lambda
→ write required order data
→ start the workflow

Payment Lambda
→ read/update payment state
→ access payment secret

Email Lambda
→ consume email messages
→ access email secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambda uses execution roles to determine what AWS resources a function can access.&lt;/p&gt;

&lt;p&gt;Each component should receive the permissions required for its responsibility, not unrestricted authority over the whole application.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email Lambda

CAN:
✓ consume email queue
✓ read email-provider credentials

CANNOT:
✗ modify payment records
✗ read payment secrets
✗ delete orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That reduces the blast radius if one component is compromised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets are not configuration strings in Git
&lt;/h2&gt;

&lt;p&gt;This should never happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PAYMENT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live-secret-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside source control.&lt;/p&gt;

&lt;p&gt;Sensitive credentials can be stored in AWS Secrets Manager and accessed only by functions whose IAM role allows it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment Lambda
      ↓
Secrets Manager
      ↓
payment-provider credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not need permission to read those credentials.&lt;/p&gt;

&lt;p&gt;Authorization boundaries matter just as much as network/application boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  And don't leak the secret through logs
&lt;/h2&gt;

&lt;p&gt;This:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;cvv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;paymentToken&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;creates a new security problem.&lt;/p&gt;

&lt;p&gt;Prefer operational context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"paymentAttemptId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAY-72"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROVIDER_TIMEOUT"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which takes us to our final production concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #9: Order 9001 failed. Where?
&lt;/h2&gt;

&lt;p&gt;Our order can now cross:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway
   ↓
Lambda
   ↓
DynamoDB
   ↓
Step Functions
   ↓
Inventory Lambda
   ↓
Payment Lambda
   ↓
SQS
   ↓
Email Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A customer says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“My order failed.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which component failed?&lt;/p&gt;

&lt;p&gt;Without observability, debugging a distributed system becomes archaeology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs: what happened?
&lt;/h3&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;prefer structured logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REQ-ABC-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROVIDER_TIMEOUT"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS Powertools for Lambda includes utilities for structured logging, tracing and metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlation IDs: connect the story
&lt;/h3&gt;

&lt;p&gt;Carry an identifier across components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REQ-ABC-123
     |
API Gateway
     |
CreateOrder
     |
Step Functions
     |
Payment
     |
Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now logs across several services can be correlated to the same business operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics: is the system healthy?
&lt;/h3&gt;

&lt;p&gt;Useful metrics might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda error rate
Lambda duration
Lambda throttles
Concurrent executions

Payment failure rate

SQS queue depth
Oldest message age

Failed Step Functions executions
Workflow duration

Orders stuck in PAYMENT_PENDING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the last one.&lt;/p&gt;

&lt;p&gt;Observability isn't only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;technical metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;business metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your infrastructure could technically be healthy while thousands of orders remain stuck in an invalid business state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alarms: when should humans care?
&lt;/h3&gt;

&lt;p&gt;Dashboards are useful.&lt;/p&gt;

&lt;p&gt;But humans shouldn't have to stare at them continuously.&lt;/p&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment failures suddenly &amp;gt; normal threshold

SQS oldest message &amp;gt; 5 minutes

Lambda throttling increasing

Workflow failures spike
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those signals should trigger alarms and operational action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step Functions execution history: observe the business workflow
&lt;/h3&gt;

&lt;p&gt;One advantage of explicit orchestration is that we can inspect a workflow execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CreateOrder        ✅
ReserveInventory   ✅
ProcessPayment     ❌
Retry              ❌
Retry              ✅
UpdateOrder        ✅
QueueEmail         ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of reconstructing the entire process from Lambda-to-Lambda calls, we can see the state-machine execution itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traces: where did the request spend time?
&lt;/h3&gt;

&lt;p&gt;Suppose checkout completes but takes eight seconds.&lt;/p&gt;

&lt;p&gt;Logs tell us that everything executed.&lt;/p&gt;

&lt;p&gt;Metrics tell us latency increased.&lt;/p&gt;

&lt;p&gt;Tracing helps answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway       40 ms
CreateOrder       90 ms
DynamoDB          12 ms
Inventory        140 ms
Payment         6.8 sec  ← bottleneck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambda integrates with AWS X-Ray, which can produce service maps and searchable traces for diagnosing errors and latency bottlenecks.&lt;/p&gt;

&lt;p&gt;So a useful observability mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logs
→ What happened?

Metrics
→ Is the system healthy overall?

Traces
→ Where did this request spend its time?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The architecture we ended up with
&lt;/h2&gt;

&lt;p&gt;We started here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway
    ↓
Lambda
    ↓
DynamoDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After introducing real production requirements, we arrived at something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         Client
                           |
                           v
                     API Gateway
                           |
                           v
                  CreateOrder Lambda
                           |
                           v
                       DynamoDB
                           |
                           v
                    Step Functions
                    /      |      \
                   /       |       \
                  v        v        v
             Inventory   Payment   Other Tasks
                 |          |
                 |      Retry / Catch
                 |          |
                 +---- Saga compensation
                            |
                        Success
                            |
                            v
                           SQS
                            |
                            v
                       Email Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Surrounding that application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IAM
→ least-privilege permissions

Secrets Manager
→ credentials

CloudWatch
→ logs, metrics, alarms

X-Ray / tracing
→ request path and latency

Correlation IDs
→ connect one order across components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is that we didn't start by saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Let's use API Gateway, Lambda, DynamoDB, SQS, Step Functions and ten other AWS services.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every component appeared because a requirement exposed a limitation in the simpler design.&lt;/p&gt;

&lt;p&gt;We needed asynchronous side effects.&lt;/p&gt;

&lt;p&gt;→ SQS.&lt;/p&gt;

&lt;p&gt;Retries introduced duplicate operations.&lt;/p&gt;

&lt;p&gt;→ Idempotency.&lt;/p&gt;

&lt;p&gt;The process became a business workflow.&lt;/p&gt;

&lt;p&gt;→ Step Functions.&lt;/p&gt;

&lt;p&gt;Distributed operations couldn't share one rollback.&lt;/p&gt;

&lt;p&gt;→ Saga and compensation.&lt;/p&gt;

&lt;p&gt;Independent work didn't need serial execution.&lt;/p&gt;

&lt;p&gt;→ Parallel and Map.&lt;/p&gt;

&lt;p&gt;Automatic scaling threatened downstream systems.&lt;/p&gt;

&lt;p&gt;→ Concurrency controls and queues.&lt;/p&gt;

&lt;p&gt;DynamoDB needed multiple efficient read patterns.&lt;/p&gt;

&lt;p&gt;→ PK/SK/GSI design.&lt;/p&gt;

&lt;p&gt;Distributed reads did not always become visible simultaneously.&lt;/p&gt;

&lt;p&gt;→ Consistency awareness.&lt;/p&gt;

&lt;p&gt;More components increased security exposure.&lt;/p&gt;

&lt;p&gt;→ Least privilege and secrets management.&lt;/p&gt;

&lt;p&gt;More components made failures harder to diagnose.&lt;/p&gt;

&lt;p&gt;→ Observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The serverless lesson I would keep
&lt;/h2&gt;

&lt;p&gt;Serverless is often introduced as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“You don't manage servers.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's true, but it is only the beginning.&lt;/p&gt;

&lt;p&gt;You still have to reason about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failure
retries
duplicates
idempotency
concurrency
downstream capacity
distributed transactions
compensation
data access patterns
consistency
permissions
security
observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS can manage servers for us.&lt;/p&gt;

&lt;p&gt;AWS cannot decide our business guarantees for us.&lt;/p&gt;

&lt;p&gt;And perhaps the most useful architectural question throughout this entire example was not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Which AWS service should I use?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What problem does my current simple design fail to solve next?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question is what turns a collection of serverless services into an architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5cweken7znyw6uo1pza.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5cweken7znyw6uo1pza.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>aws</category>
      <category>serverless</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>Building a Website Change Monitoring System: From One URL to a Distributed Crawling Pipeline</title>
      <dc:creator>Maz I</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:27:40 +0000</pubDate>
      <link>https://dev.to/codewithmaz/building-a-website-change-monitoring-system-from-one-url-to-a-distributed-crawling-pipeline-471k</link>
      <guid>https://dev.to/codewithmaz/building-a-website-change-monitoring-system-from-one-url-to-a-distributed-crawling-pipeline-471k</guid>
      <description>&lt;p&gt;&lt;strong&gt;Building a Website Change Monitoring System: From One URL to a Distributed Crawling Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine someone gives us a URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tell me if this website changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first, this sounds almost trivial.&lt;/p&gt;

&lt;p&gt;Fetch the page, save the HTML, come back later, fetch it again, compare the two versions.&lt;/p&gt;

&lt;p&gt;For one page, that can work.&lt;/p&gt;

&lt;p&gt;But almost immediately, questions start appearing.&lt;/p&gt;

&lt;p&gt;What exactly are we monitoring?&lt;/p&gt;

&lt;p&gt;Only the homepage?&lt;/p&gt;

&lt;p&gt;Every page under the website?&lt;/p&gt;

&lt;p&gt;How often should we check it?&lt;/p&gt;

&lt;p&gt;Every two minutes?&lt;/p&gt;

&lt;p&gt;Once per day?&lt;/p&gt;

&lt;p&gt;Only once when an administrator requests it?&lt;/p&gt;

&lt;p&gt;What happens when a harmless calendar or rotating banner changes?&lt;/p&gt;

&lt;p&gt;And if we eventually monitor hundreds or thousands of websites, who decides &lt;strong&gt;what should be crawled&lt;/strong&gt;, and who actually performs all that crawling?&lt;/p&gt;

&lt;p&gt;Those questions are what turn a simple script into a monitoring system.&lt;/p&gt;

&lt;p&gt;This article walks through that evolution one design decision at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  First question: what does “monitor this website” actually mean?
&lt;/h2&gt;

&lt;p&gt;Suppose we start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we fetch only that URL, we are monitoring only the homepage.&lt;/p&gt;

&lt;p&gt;But the important page might actually be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/products/important-product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So monitoring a website introduces our first configuration decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  How deeply should we crawl?
&lt;/h2&gt;

&lt;p&gt;Consider this website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
│
├── About
├── Products
│   ├── Product A
│   └── Product B
│
└── Blog
    ├── Post 1
    └── Post 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we monitor only the homepage, we might completely miss a change on &lt;code&gt;Product A&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So we could tell the crawler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Follow every internal link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that creates another problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Product A&lt;/code&gt; may contain links to documentation.&lt;/p&gt;

&lt;p&gt;Documentation may contain hundreds of pages.&lt;/p&gt;

&lt;p&gt;Those pages may link to thousands more.&lt;/p&gt;

&lt;p&gt;Suddenly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then potentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10,000+ URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we need a boundary.&lt;/p&gt;

&lt;p&gt;That is what &lt;strong&gt;crawl depth&lt;/strong&gt; gives us.&lt;/p&gt;

&lt;p&gt;If the administrator chooses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we monitor only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
    ↓
direct internal links
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
    ↓
Level 1 pages
    ↓
links discovered inside Level 1 pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Depth 0

example.com
      |
      v

Depth 1

/about
/products
/blog
      |
      v

Depth 2

/products/a
/products/b
/blog/post-1
/blog/post-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the administrator can choose the monitoring surface instead of letting the crawler wander indefinitely.&lt;/p&gt;

&lt;p&gt;This also tells us something important about our data model.&lt;/p&gt;

&lt;p&gt;A website isn't just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs configuration.&lt;/p&gt;

&lt;p&gt;Something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Website
├── URL
├── Crawl depth
├── Monitoring frequency
├── Ignore rules
├── Notification preferences
└── Monitoring enabled/disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we are no longer building a crawler.&lt;/p&gt;

&lt;p&gt;We are beginning to build a &lt;strong&gt;monitoring product&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second question: how often should we check?
&lt;/h2&gt;

&lt;p&gt;Suppose our baseline crawl finishes at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When should we crawl again?&lt;/p&gt;

&lt;p&gt;There is no universally correct answer.&lt;/p&gt;

&lt;p&gt;For a low-risk informational website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;once per day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might be enough.&lt;/p&gt;

&lt;p&gt;For something sensitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;every 5 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might be appropriate.&lt;/p&gt;

&lt;p&gt;For another system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;every 2 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might be required.&lt;/p&gt;

&lt;p&gt;And sometimes continuous monitoring isn't even needed.&lt;/p&gt;

&lt;p&gt;An administrator may simply want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl once now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So frequency also belongs to the website's monitoring configuration.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com

crawl depth: 2
frequency: every 5 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;another-site.com

crawl depth: 1
frequency: daily
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates our next architectural requirement.&lt;/p&gt;

&lt;p&gt;Something needs to keep asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which websites are due to be checked now?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That responsibility belongs to a &lt;strong&gt;scheduler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Website configuration
        |
        v
    Scheduler
        |
        v
Is next crawl due?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scheduler doesn't need to know anything about HTML parsing or link discovery.&lt;/p&gt;

&lt;p&gt;Its job is simply to decide &lt;strong&gt;when work should happen&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That separation becomes important later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third question: how do we avoid becoming the attacker?
&lt;/h2&gt;

&lt;p&gt;Now imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 3
frequency = 2 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the website contains thousands of pages.&lt;/p&gt;

&lt;p&gt;A badly designed crawler could start firing hundreds of requests against the same server every two minutes.&lt;/p&gt;

&lt;p&gt;Our monitoring system could accidentally create the exact availability problem it is supposed to help detect.&lt;/p&gt;

&lt;p&gt;So monitoring frequency cannot mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hit the website as aggressively as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The crawler needs to behave responsibly.&lt;/p&gt;

&lt;p&gt;A mature design should consider things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;per-domain concurrency limits
request delays
timeouts
maximum pages per crawl
retry backoff
429 handling
5xx handling
overlapping crawl prevention
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com

maximum concurrent requests = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than letting dozens of workers hammer the same domain simultaneously.&lt;/p&gt;

&lt;p&gt;And if the previous crawl is still running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl #101 = running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we probably shouldn't blindly start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl #102
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;just because the next two-minute interval arrived.&lt;/p&gt;

&lt;p&gt;Already, the “simple crawler” has acquired scheduling and resource-control requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fourth question: what happens when hundreds of websites become due together?
&lt;/h2&gt;

&lt;p&gt;Suppose we have 1,000 monitored websites.&lt;/p&gt;

&lt;p&gt;At 10:00, hundreds become eligible for crawling.&lt;/p&gt;

&lt;p&gt;One naive implementation would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dashboard
   |
   v
Fetch website
   |
   v
Parse HTML
   |
   v
Save results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But now the application serving administrators is also responsible for long-running crawling work.&lt;/p&gt;

&lt;p&gt;That creates several problems.&lt;/p&gt;

&lt;p&gt;A slow website could tie up the main application.&lt;/p&gt;

&lt;p&gt;A timeout could delay unrelated work.&lt;/p&gt;

&lt;p&gt;A burst of scheduled crawls could overload the dashboard server.&lt;/p&gt;

&lt;p&gt;And scaling the crawler would mean scaling the entire application.&lt;/p&gt;

&lt;p&gt;So logically, we separate two responsibilities.&lt;/p&gt;

&lt;p&gt;The main application manages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customers
websites
configuration
monitoring settings
administration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crawler handles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;network requests
HTML
link extraction
snapshots
change detection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the system I worked on, the main application kept its structured application data in &lt;strong&gt;MySQL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Crawler-oriented data was handled separately.&lt;/p&gt;

&lt;p&gt;But now these two parts need a way to communicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is where the queue appears
&lt;/h2&gt;

&lt;p&gt;Instead of telling the crawler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Crawl this website right now and make me wait until you're done,&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the application can publish a job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"websiteId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxDepth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into a message queue.&lt;/p&gt;

&lt;p&gt;Now the architecture evolves naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dashboard
   |
   v
 MySQL
   |
   v
Scheduler
   |
   v
Crawl Queue
   |
   v
Crawler Workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us a buffer.&lt;/p&gt;

&lt;p&gt;If 500 crawl jobs appear suddenly, the main application doesn't need 500 crawlers immediately.&lt;/p&gt;

&lt;p&gt;Jobs wait in the queue.&lt;/p&gt;

&lt;p&gt;Crawler workers consume them according to available capacity.&lt;/p&gt;

&lt;p&gt;If we later need more crawling throughput, we can add crawler workers without redesigning the dashboard.&lt;/p&gt;

&lt;p&gt;The queue isn't there because queues are fashionable.&lt;/p&gt;

&lt;p&gt;It appears because &lt;strong&gt;scheduled work and execution capacity are different problems&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the crawler finally receives a URL
&lt;/h2&gt;

&lt;p&gt;Suppose the worker receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first operation is straightforward.&lt;/p&gt;

&lt;p&gt;Fetch the page.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this is the first crawl.&lt;/p&gt;

&lt;p&gt;There is nothing to compare against yet.&lt;/p&gt;

&lt;p&gt;So instead of detecting a change, this crawl establishes a &lt;strong&gt;baseline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We store the page content.&lt;/p&gt;

&lt;p&gt;In our crawler side, page snapshots and crawling data were stored in &lt;strong&gt;MongoDB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now the architecture has two distinct data concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MySQL
↓
website/customer/configuration data

MongoDB
↓
crawler/page/snapshot-oriented data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation wasn't simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SQL good here, MongoDB good there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two sides represented different workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The homepage is only the beginning
&lt;/h2&gt;

&lt;p&gt;Once the homepage is downloaded, we inspect its internal links.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/about"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/products"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Products&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/blog"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Blog&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We extract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/about
/products
/blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;convert them to full URLs where necessary, and keep only links belonging to the target website.&lt;/p&gt;

&lt;p&gt;Then we need another important step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deduplication&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine the same page appears through several navigation paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage → Products
Blog → Products
About → Products
Footer → Products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don't want to crawl &lt;code&gt;/products&lt;/code&gt; four times during the same crawl run.&lt;/p&gt;

&lt;p&gt;So logically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract links
      |
      v
Normalize URLs
      |
      v
Remove duplicates
      |
      v
Check crawl depth
      |
      v
Schedule inner pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inner crawler then processes those URLs.&lt;/p&gt;

&lt;p&gt;Each inner page can discover more URLs.&lt;/p&gt;

&lt;p&gt;So a queue item might carry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxDepth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;depth &amp;lt; maxDepth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we extract more links.&lt;/p&gt;

&lt;p&gt;When:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;depth == maxDepth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we stop expanding.&lt;/p&gt;

&lt;p&gt;That simple number prevents recursive discovery from turning into uncontrolled crawling.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL deduplication is trickier than it looks
&lt;/h2&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/about
https://example.com/about/
https://example.com/about#team
https://example.com/about?utm_source=email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Are those four separate pages?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;But often they represent the same useful monitoring target.&lt;/p&gt;

&lt;p&gt;So before deduplicating, a modern crawler may normalize URLs by handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fragments
tracking parameters
relative paths
trailing slashes
canonical URLs
host casing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, the crawler may spend significant resources repeatedly monitoring effectively identical pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now we finally reach the actual monitoring problem
&lt;/h2&gt;

&lt;p&gt;After the first crawl, suppose we stored this page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five minutes later, according to its configured frequency, the scheduler queues the website again.&lt;/p&gt;

&lt;p&gt;The crawler downloads the page again.&lt;/p&gt;

&lt;p&gt;Now we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;previous version
current version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Has anything changed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our implementation, one simple mechanism was an &lt;strong&gt;MD5 hash&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML
 |
 v
MD5
 |
 v
hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So baseline HTML produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A7F91...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the next crawl produces another hash.&lt;/p&gt;

&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_hash == new_hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the inputs are identical.&lt;/p&gt;

&lt;p&gt;Nothing changed.&lt;/p&gt;

&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_hash != new_hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;something changed.&lt;/p&gt;

&lt;p&gt;Very simple.&lt;/p&gt;

&lt;p&gt;Very fast.&lt;/p&gt;

&lt;p&gt;And also incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Because “different” does not mean “important”
&lt;/h2&gt;

&lt;p&gt;Imagine this element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    9 August
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A day later it becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    10 August
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTML changed.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old MD5 != new MD5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system alerts the administrator.&lt;/p&gt;

&lt;p&gt;But there was no attack.&lt;/p&gt;

&lt;p&gt;Nothing important happened.&lt;/p&gt;

&lt;p&gt;Now imagine another page contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:31:04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and one minute later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:32:04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other common examples include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rotating headlines
advertisements
visitor counters
timestamps
calendars
live market values
random identifiers
dynamic widgets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reveals the central weakness of pure hashing.&lt;/p&gt;

&lt;p&gt;A hash can answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are these two inputs identical?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It cannot answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this difference meaningful?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To MD5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calendar date changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;attacker replaced the homepage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are both simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;different input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that creates &lt;strong&gt;false positives&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why false positives are dangerous
&lt;/h2&gt;

&lt;p&gt;Imagine monitoring a security-sensitive website.&lt;/p&gt;

&lt;p&gt;The administrator receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALERT
ALERT
ALERT
ALERT
ALERT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;throughout the day.&lt;/p&gt;

&lt;p&gt;Most alerts are harmless calendar or headline changes.&lt;/p&gt;

&lt;p&gt;Eventually the administrator begins ignoring them.&lt;/p&gt;

&lt;p&gt;Now when an actual unexpected modification happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALERT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it looks like everything else.&lt;/p&gt;

&lt;p&gt;So false-positive handling isn't merely a convenience feature.&lt;/p&gt;

&lt;p&gt;It directly affects whether the monitoring system remains useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing the ignore list
&lt;/h2&gt;

&lt;p&gt;Suppose we know this region changes constantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    10 August
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The administrator can configure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ignore .calendar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ignore .ticker
ignore #clock
ignore .rotating-banner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now our comparison pipeline becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fetched HTML
     |
     v
Apply ignore rules
     |
     v
Remove known dynamic regions
     |
     v
Generate normalized content
     |
     v
MD5
     |
     v
Compare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of hashing everything blindly, we're hashing the content we actually care about monitoring.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prepare_for_monitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignored_selectors&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ignored_selectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;normalized_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepare_for_monitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.calendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.ticker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;page_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a calendar update doesn't automatically become a security alert.&lt;/p&gt;

&lt;p&gt;This is where the system moves from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;change detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;toward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;meaningful change detection&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  But what happens when a meaningful change is detected?
&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_hash != new_hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;after ignore rules have been applied.&lt;/p&gt;

&lt;p&gt;The crawler has discovered something.&lt;/p&gt;

&lt;p&gt;Should it now send an email itself?&lt;/p&gt;

&lt;p&gt;Send an SMS itself?&lt;/p&gt;

&lt;p&gt;Update the dashboard itself?&lt;/p&gt;

&lt;p&gt;It could.&lt;/p&gt;

&lt;p&gt;But then the crawler would be responsible for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP crawling
HTML parsing
comparison
email
SMS
dashboard updates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's too many responsibilities in one component.&lt;/p&gt;

&lt;p&gt;So another boundary naturally appears.&lt;/p&gt;

&lt;p&gt;The crawler emits a &lt;strong&gt;change event&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Change detected
      |
      v
Alert Queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then separate notification handlers can deliver it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Alert Queue
                  |
       +----------+----------+
       |          |          |
       v          v          v
   Dashboard    Email       SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now an SMS provider outage doesn't stop crawling.&lt;/p&gt;

&lt;p&gt;Email delivery can retry independently.&lt;/p&gt;

&lt;p&gt;And a new notification channel can be added later without rewriting the crawler.&lt;/p&gt;

&lt;p&gt;Again, the queue isn't introduced because “event-driven architecture is cool.”&lt;/p&gt;

&lt;p&gt;It appears because &lt;strong&gt;detecting something and notifying someone are separate reliability problems&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture we ended up with
&lt;/h2&gt;

&lt;p&gt;By following the requirements rather than starting from technologies, our simple URL checker evolved into something closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Administrator
                         |
                         v
                    Dashboard
                         |
                         v
                       MySQL
             website/configuration
                         |
                         v
                     Scheduler
                         |
               Is crawl due?
                         |
                         v
                    Crawl Queue
                         |
                         v
                  Python Crawlers
                         |
              +----------+----------+
              |                     |
              v                     v
        Link Discovery          Page Snapshot
              |                     |
              v                     v
      Normalize/Deduplicate       MongoDB
              |
              v
        Depth-controlled
          inner crawling

                         |
                         v
                  Prepare HTML
                apply ignore list
                         |
                         v
                    Generate MD5
                         |
                         v
               Compare previous hash
                    /        \
                   /          \
              unchanged      changed
                                |
                                v
                           Alert Queue
                                |
                    +-----------+-----------+
                    |           |           |
                    v           v           v
                Dashboard      Email       SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What began as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;download HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;became scheduling, crawling, discovery, state, comparison, noise reduction, queueing, and notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more problem: crawl politely
&lt;/h2&gt;

&lt;p&gt;Let's return to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frequency = 2 minutes
depth = 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose this website has 5,000 discoverable pages.&lt;/p&gt;

&lt;p&gt;We absolutely don't want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5,000 requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fired aggressively against the target.&lt;/p&gt;

&lt;p&gt;A responsible crawler needs controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-domain concurrency
&lt;/h3&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 workers → example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we might enforce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maximum 2 concurrent requests → example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Crawl budgets
&lt;/h3&gt;

&lt;p&gt;A website configuration could include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max depth: 3
max pages per crawl: 1,000
request timeout: 10 seconds
per-domain concurrency: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depth and page limit solve different problems.&lt;/p&gt;

&lt;p&gt;Depth 1 could still contain 20,000 links.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry with backoff
&lt;/h3&gt;

&lt;p&gt;If the target returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 Too Many Requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;retrying immediately is the wrong response.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1s
2s
4s
8s
16s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with jitter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't overlap crawls blindly
&lt;/h3&gt;

&lt;p&gt;If monitoring is configured every two minutes but one crawl takes four minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00 crawl A starts
10:02 crawl B scheduled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the scheduler should probably detect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;website currently crawling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than blindly launching another run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change if I designed it today
&lt;/h2&gt;

&lt;p&gt;The underlying problem is still interesting, but I wouldn't rebuild every detail exactly the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hash first, diff second
&lt;/h3&gt;

&lt;p&gt;Hashing is still useful for a very fast first check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash same
    |
    v
stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash different
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would generate an actual structured diff.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Website changed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the administrator might see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- Payment destination: account A
&lt;/span&gt;&lt;span class="gi"&gt;+ Payment destination: account B
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's significantly more useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare the DOM, not only serialized HTML
&lt;/h3&gt;

&lt;p&gt;HTML already contains structure.&lt;/p&gt;

&lt;p&gt;So changes could be classified by region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;title changed
main content changed
form action changed
external script added
navigation changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then different changes could receive different severity levels.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calendar changed              INFO
Headline changed              LOW
Form action changed           HIGH
New external JavaScript       HIGH
Large DOM replacement         CRITICAL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Better normalization
&lt;/h3&gt;

&lt;p&gt;Before comparison I would also normalize things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whitespace
volatile generated IDs
known tracking parameters
timestamps
irrelevant markup differences
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would further reduce noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explicit crawl-run tracking
&lt;/h3&gt;

&lt;p&gt;Instead of only knowing whether a website is “currently crawling,” I would model crawl runs explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl_run_id
website_id
started_at
finished_at
pages_discovered
pages_processed
pages_failed
status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes operational debugging much easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dead-letter queues
&lt;/h3&gt;

&lt;p&gt;Some crawl jobs will repeatedly fail.&lt;/p&gt;

&lt;p&gt;After controlled retries, they should move somewhere visible instead of looping forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normal queue
     |
   retries
     |
     v
dead-letter queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Observability
&lt;/h3&gt;

&lt;p&gt;For a serious deployment I'd monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queue depth
crawl duration
pages/sec
HTTP error rate
retry count
change rate
false-positive rate
notification failures
per-domain request rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without those metrics, it's difficult to know whether the system is healthy or merely running.&lt;/p&gt;

&lt;h2&gt;
  
  
  And today, AI creates an interesting extra layer
&lt;/h2&gt;

&lt;p&gt;The deterministic monitoring engine should remain deterministic.&lt;/p&gt;

&lt;p&gt;I would &lt;strong&gt;not&lt;/strong&gt; replace HTML comparison with an LLM.&lt;/p&gt;

&lt;p&gt;But after a real change has already been detected, AI could help answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does this change mean?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- &amp;lt;script src="/assets/app.js"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;script src="https://unknown-example.com/inject.js"&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A semantic analysis stage might classify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"external_script_added"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A previously unseen external JavaScript source was introduced."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deterministic detection
        |
        v
Real change found
        |
        v
Generate structured diff
        |
        v
Rules / classifiers
        |
        v
Optional AI analysis
        |
        v
Severity + explanation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the ordering.&lt;/p&gt;

&lt;p&gt;AI helps interpret the signal.&lt;/p&gt;

&lt;p&gt;It doesn't replace the reliable mechanism that discovers the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting lesson
&lt;/h2&gt;

&lt;p&gt;When we began, the requirement looked like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Monitor a URL for changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But each real-world question forced another design decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which pages matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ crawl depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How frequently do they matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ monitoring schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when hundreds become due?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ queue + workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we revisit inner pages?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ link extraction + deduplication + depth tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we know something changed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ snapshots + hashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we avoid useless alerts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ ignore rules + normalization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we notify reliably?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ alert events + separate notification workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we avoid harming the monitored site?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ crawl budgets, rate limits, concurrency control, and backoff.&lt;/p&gt;

&lt;p&gt;That's what I find most interesting about systems like this.&lt;/p&gt;

&lt;p&gt;The final architecture doesn't need to be invented on a whiteboard first.&lt;/p&gt;

&lt;p&gt;It can emerge naturally from repeatedly asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What problem does our current simple solution fail to solve next?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>systemdesign</category>
      <category>python</category>
      <category>architecture</category>
      <category>webdelopment</category>
    </item>
    <item>
      <title>Why does PostgreSQL sometimes ignore an index you created?</title>
      <dc:creator>Maz I</dc:creator>
      <pubDate>Sat, 08 Aug 2026 22:55:41 +0000</pubDate>
      <link>https://dev.to/codewithmaz/why-does-postgresql-sometimes-ignore-an-index-you-created-6pa</link>
      <guid>https://dev.to/codewithmaz/why-does-postgresql-sometimes-ignore-an-index-you-created-6pa</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why does PostgreSQL sometimes ignore an index you created?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A common assumption is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If a column has an index, PostgreSQL should use it.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But PostgreSQL does not work that way.&lt;/p&gt;

&lt;p&gt;An index is only one possible access path.&lt;/p&gt;

&lt;p&gt;Before executing a query, PostgreSQL's query planner estimates the cost of different plans and chooses the one it believes will be cheapest.&lt;/p&gt;

&lt;p&gt;That might be:&lt;/p&gt;

&lt;p&gt;• Sequential Scan&lt;br&gt;
• Index Scan&lt;br&gt;
• Index Only Scan&lt;br&gt;
• Bitmap Index Scan&lt;br&gt;
• Parallel Sequential Scan&lt;/p&gt;

&lt;p&gt;So sometimes PostgreSQL sees your perfectly valid index and deliberately decides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Scanning the table is cheaper.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_status&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And imagine the table contains 10 million users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;active   = 7,000,000
inactive = 2,900,000
banned   =   100,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare these queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'banned'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both use the indexed &lt;code&gt;status&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;But PostgreSQL may choose very different execution plans.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;banned&lt;/code&gt;, only around 1% of rows match.&lt;/p&gt;

&lt;p&gt;Using the index can make sense because PostgreSQL can locate a relatively small set of rows instead of scanning millions of unrelated rows.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;active&lt;/code&gt;, around 70% of the table matches.&lt;/p&gt;

&lt;p&gt;Now using the index may mean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Traverse the index.&lt;/li&gt;
&lt;li&gt;Find millions of matching row locations.&lt;/li&gt;
&lt;li&gt;Visit millions of table pages to retrieve those rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At that point, reading the table sequentially may simply cost less.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;selectivity&lt;/strong&gt; becomes important.&lt;/p&gt;

&lt;p&gt;A highly selective condition returns a small percentage of the table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user@example.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is usually a great candidate for an index.&lt;/p&gt;

&lt;p&gt;A low-selectivity condition might match most of the table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An index may provide little advantage.&lt;/p&gt;

&lt;p&gt;But how does PostgreSQL know how many rows are likely to match?&lt;/p&gt;

&lt;h3&gt;
  
  
  Statistics.
&lt;/h3&gt;

&lt;p&gt;PostgreSQL collects information about the data distribution in a table.&lt;/p&gt;

&lt;p&gt;The planner uses those statistics to estimate things such as:&lt;/p&gt;

&lt;p&gt;• how many rows a condition will match&lt;br&gt;
• common values&lt;br&gt;
• value distribution&lt;br&gt;
• number of distinct values&lt;br&gt;
• relationships that affect selectivity&lt;/p&gt;

&lt;p&gt;That is why stale statistics can result in poor plans.&lt;/p&gt;

&lt;p&gt;You can refresh them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this brings us to one of the most useful tools for PostgreSQL performance work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'banned'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt; shows the plan PostgreSQL intends to use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; actually executes the query and shows what really happened.&lt;/p&gt;

&lt;p&gt;Two numbers I pay particular attention to are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated rows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What PostgreSQL thought would happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actual rows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What really happened.&lt;/p&gt;

&lt;p&gt;If the planner estimates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows = 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the query actually returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows = 100,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that difference is a clue.&lt;/p&gt;

&lt;p&gt;The planner may be making decisions using an inaccurate picture of the data.&lt;/p&gt;

&lt;p&gt;There is another important misconception:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More indexes do not automatically mean faster databases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every index has a cost.&lt;/p&gt;

&lt;p&gt;When you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL may also need to update the relevant indexes.&lt;/p&gt;

&lt;p&gt;Indexes consume storage, add write overhead, and create additional structures the planner has to consider.&lt;/p&gt;

&lt;p&gt;So the goal is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Index every column used in a WHERE clause.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify a slow query.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Understand the execution plan.&lt;/li&gt;
&lt;li&gt;Check row estimates and actual rows.&lt;/li&gt;
&lt;li&gt;Look at selectivity and data distribution.&lt;/li&gt;
&lt;li&gt;Decide whether the query or index should change.&lt;/li&gt;
&lt;li&gt;Measure again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Composite indexes introduce another layer.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_status&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order of columns matters because the index structure is organized around those columns.&lt;/p&gt;

&lt;p&gt;And sometimes PostgreSQL can combine multiple indexes using bitmap scans rather than using one composite index.&lt;/p&gt;

&lt;p&gt;There are also partial indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_pending_orders&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of indexing every order, you can index only the subset important to a particular workload.&lt;/p&gt;

&lt;p&gt;And covering indexes can sometimes allow PostgreSQL to answer a query without visiting the table heap at all.&lt;/p&gt;

&lt;p&gt;The deeper I go into PostgreSQL performance, the more one idea stands out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An index does not tell PostgreSQL what to do.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It gives the planner another option.&lt;/p&gt;

&lt;p&gt;The planner still has to decide whether that option is actually cheaper.&lt;/p&gt;

&lt;p&gt;So when PostgreSQL ignores an index, I would not immediately ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Why isn't PostgreSQL using my index?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I would first ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“What does PostgreSQL know about my data that makes another plan look cheaper?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question usually leads to a much more interesting investigation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxglcukfcxhcsb5iz7my.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxglcukfcxhcsb5iz7my.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>databaseperformance</category>
      <category>node</category>
    </item>
  </channel>
</rss>
