Simulate Responsive Post Systems

Build a browser simulator for async posting, retries, and feed fan-out.

Introduction

30 Second Summary

A post can feel broken when the screen stays unchanged for a few seconds. The same work feels faster when the interface responds before the slow parts finish.

In this project, you will build a browser-based posting lab inspired by Instagram that compares a blocking request with asynchronous processing. It distinguishes verified Instagram engineering practices from the generic teaching model.

What You'll Build

You run each posting flow while the simulator exposes the gap between the first visible state, the server acknowledgement, and completed publication.

By the end of this project, you'll have:

  • An interactive posting simulator that reveals each design's critical path across the Client, API, Queue, Worker, and Feed lanes.
  • A side-by-side latency comparison that preserves the latest Blocking and Responsive measurements for all three milestones.
  • A retry demonstration that uses idempotency to preserve one visible post after a lost worker acknowledgement.
  • Secret Mission: A toy model that compares fan-out-on-write with fan-out-on-read as follower and active-reader counts change.

Are there any prerequisites?

The main prerequisites are production experience with software systems and light JavaScript familiarity. All code is provided for a Mac workflow using Visual Studio Code and Safari.

Before We Start

Before the hands-on work begins, this step locks in the simulator's purpose. You are committing to demonstrate how immediate pending UI and asynchronous processing reduce perceived posting latency while separating verified Instagram practices from generic system-design patterns.

Build the Simulator Shell

A useful latency comparison needs one shared surface where each stage can report its state. Without that baseline, the blocking path and responsive path would be difficult to compare.

You will build the local simulator shell with HTML, CSS, and JavaScript. You will open it in Safari to confirm that every lane and measurement starts in a neutral state.

In this step, get ready to:
  • Create the three local files for the simulator.
  • Build and style the controls, system lanes, feed, metrics, comparison table, and event log.
  • Initialize the simulator state and verify the complete shell in Safari.
Create the project shell

The posting-speed-simulator folder keeps the page structure, visual design, and browser behavior separate. Visual Studio Code treats the opened folder as one workspace.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code and press Return.
  • Click File in the menu bar.
  • Click Open Folder....
  • Select Desktop as the location.
  • Click New Folder.
  • Enter posting-speed-simulator as the folder name.
  • Press Return to create the folder.

What if Workspace Trust appears?

Visual Studio Code may ask whether you trust the folder. You created this local folder yourself, so select Yes, I trust the authors to enable the workspace features.

  • Click Open to load the posting-speed-simulator folder.
  • Click New File... in the Explorer view.
  • Enter index.html and press Return.
  • Click New File... again.
  • Enter style.css and press Return.
  • Click New File... once more.
  • Enter app.js and press Return.

You should now see index.html, style.css, and app.js inside the Explorer view.

Files in the wrong folder?

Confirm that all three files appear directly beneath posting-speed-simulator in the Explorer. Delete any file created outside that folder before recreating it with New File....

Help me fix my Visual Studio Code file structure.

The first HTML chunk establishes the document and its disclaimer. The disclaimer keeps this generic teaching model separate from claims about Instagram's private upload pipeline.

  • Select index.html in the Explorer.
  • Create the document shell by pasting this code:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Why Posting Feels Instant</title>
  <link rel="stylesheet" href="style.css">
  <script src="app.js" defer></script>
</head>
<body>
  <main class="page-shell">
    <header class="hero">
      <p class="eyebrow">Instagram-inspired system design lab</p>
      <h1>Why Posting Feels Instant</h1>
      <p class="lede">Compare a blocking request with an immediate pending state and queued background work.</p>
      <p class="disclaimer">This is a generic teaching model. It is not a claim about Instagram's private upload pipeline.</p>
    </header>
  </main>
</body>
</html>

What does this HTML do?

  • The head connects style.css and defers app.js until the page structure is ready.
  • The hero introduces the experiment and states its evidence boundary.
  • The page-shell becomes the container for every simulator panel.
  • Save index.html by pressing Cmd+S.
  • Control-click index.html in the Explorer.
  • Select Reveal in Finder.
  • Control-click index.html in Finder.
  • Select Open With.
  • Select Safari.

You should see the project title, its short description, and the evidence-boundary disclaimer in Safari.

Seeing a blank page?

  • Confirm that index.html contains the opening and closing html tags.
  • Confirm that Safari opened the copy inside posting-speed-simulator.

Help me debug my blank HTML shell.

The controls collect the simulated caption and failure choice. The five lanes make each part of the posting path visible as its state changes.

  • Switch back to index.html in Visual Studio Code.
  • Find the </header> line.
  • Add the controls and lanes directly below that line by pasting this code:
    <section class="panel controls" aria-labelledby="controls-title">
      <h2 id="controls-title">Run a posting flow</h2>
      <label for="caption">Caption</label>
      <input id="caption" type="text" value="A fast-feeling post" maxlength="80">
      <label class="check-row">
        <input id="fail-first" type="checkbox" checked>
        Fail first worker acknowledgement
      </label>
      <div class="button-row">
        <button id="run-blocking" type="button">Run blocking flow</button>
        <button id="run-async" type="button">Run async flow</button>
        <button id="reset-simulator" class="secondary" type="button">Reset all results</button>
      </div>
    </section>

    <section class="panel" aria-labelledby="lanes-title">
      <h2 id="lanes-title">System lanes</h2>
      <div class="lanes">
        <article class="lane"><h3>Client</h3><p id="client-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>API</h3><p id="api-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>Queue</h3><p id="queue-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>Worker</h3><p id="worker-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>Feed</h3><p id="feed-state" data-tone="idle">Idle</p></article>
      </div>
    </section>

What do these controls represent?

  • The caption field supplies the text used by the simulated post.
  • The failure checkbox prepares the later retry experiment.
  • The Client, API, Queue, Worker, and Feed lanes expose where work is happening.
  • Save index.html.
  • Return to Safari.
  • Reload the local page.

You should now see the caption field, the failure checkbox, three controls, and five lanes that each read Idle.

Missing a lane or control?

Check that the controls section and lanes section both sit inside <main class="page-shell">. Confirm that every opening section tag has its closing tag.

Help me find the missing HTML element.

The dashboard separates the simulated feed from the current run measurements. This makes the first visible post state and each latency milestone easy to compare.

  • Switch back to index.html.
  • Find the </main> line.
  • Add the dashboard directly above that line by pasting this code:
    <section class="dashboard">
      <article class="panel feed-panel">
        <h2>Simulated feed</h2>
        <p id="empty-feed" class="muted">No post is visible yet.</p>
        <div id="post-card" class="post-card" hidden>
          <div class="photo-placeholder" aria-label="Abstract photo placeholder"></div>
          <div>
            <strong id="post-caption">A fast-feeling post</strong>
            <p id="post-status" class="status-chip">Pending</p>
          </div>
        </div>
      </article>

      <article class="panel">
        <h2>Current run</h2>
        <div class="metrics">
          <div class="metric-card"><span>Perceived result</span><strong id="perceived-metric">Not run</strong></div>
          <div class="metric-card"><span>Server acknowledgement</span><strong id="ack-metric">Not run</strong></div>
          <div class="metric-card"><span>Publication completion</span><strong id="publication-metric">Not run</strong></div>
          <div class="metric-card"><span>Duplicates prevented</span><strong id="duplicates-metric">0</strong></div>
        </div>
      </article>
    </section>

How is the dashboard organized?

  • The feed begins with an empty message while the hidden post card waits for a later flow.
  • The post card has separate caption and status elements so JavaScript can update them independently.
  • The metric cards reserve a visible value for each timing milestone and the duplicate counter.
  • Save index.html.
  • Return to Safari.
  • Reload the local page.

You should see an empty simulated feed beside four current-run metric cards.

Post card visible too early?

Confirm that the post-card element includes the hidden attribute. The empty-feed message should remain visible before a flow runs.

Help me restore the initial feed state.

The comparison table preserves one result for each design. The event log gives later steps a place to expose the order of simulated work.

  • Switch back to index.html.
  • Find the </main> line.
  • Add the comparison, log, and extension mount directly above that line by pasting this code:
    <section class="panel" aria-labelledby="comparison-title">
      <h2 id="comparison-title">Latest measured comparison</h2>
      <p class="muted">Elapsed time is measured inside this local simulation. It is not an Instagram production benchmark.</p>
      <div class="table-scroll">
        <table class="comparison-table">
          <thead>
            <tr><th>Design</th><th>Perceived result</th><th>Server acknowledgement</th><th>Publication completion</th></tr>
          </thead>
          <tbody>
            <tr><th>Blocking</th><td id="blocking-perceived">Not run</td><td id="blocking-ack">Not run</td><td id="blocking-publication">Not run</td></tr>
            <tr><th>Responsive</th><td id="responsive-perceived">Not run</td><td id="responsive-ack">Not run</td><td id="responsive-publication">Not run</td></tr>
          </tbody>
        </table>
      </div>
    </section>

    <section class="panel" aria-labelledby="log-title">
      <h2 id="log-title">Event log</h2>
      <ol id="event-log" class="event-log"></ol>
    </section>

    <div id="secret-mission"></div>

Why preserve both comparison rows?

Each row keeps the latest measurements for one design. A later flow can update its own row without erasing the other result.

The empty secret-mission element reserves a safe extension point for the optional challenge.

  • Save index.html.
  • Return to Safari.
  • Reload the local page.

You should see Blocking and Responsive rows filled with Not run. The Event log should be empty.

Comparison table out of place?

Confirm that the comparison section, event-log section, and secret-mission mount all appear above </main>.

Help me repair the page structure.

✔️ Awesome, I've got everything!

Your complete page structure is ready. Save index.html before moving to the stylesheet.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Why Posting Feels Instant</title>
  <link rel="stylesheet" href="style.css">
  <script src="app.js" defer></script>
</head>
<body>
  <main class="page-shell">
    <header class="hero">
      <p class="eyebrow">Instagram-inspired system design lab</p>
      <h1>Why Posting Feels Instant</h1>
      <p class="lede">Compare a blocking request with an immediate pending state and queued background work.</p>
      <p class="disclaimer">This is a generic teaching model. It is not a claim about Instagram's private upload pipeline.</p>
    </header>

    <section class="panel controls" aria-labelledby="controls-title">
      <h2 id="controls-title">Run a posting flow</h2>
      <label for="caption">Caption</label>
      <input id="caption" type="text" value="A fast-feeling post" maxlength="80">
      <label class="check-row">
        <input id="fail-first" type="checkbox" checked>
        Fail first worker acknowledgement
      </label>
      <div class="button-row">
        <button id="run-blocking" type="button">Run blocking flow</button>
        <button id="run-async" type="button">Run async flow</button>
        <button id="reset-simulator" class="secondary" type="button">Reset all results</button>
      </div>
    </section>

    <section class="panel" aria-labelledby="lanes-title">
      <h2 id="lanes-title">System lanes</h2>
      <div class="lanes">
        <article class="lane"><h3>Client</h3><p id="client-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>API</h3><p id="api-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>Queue</h3><p id="queue-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>Worker</h3><p id="worker-state" data-tone="idle">Idle</p></article>
        <article class="lane"><h3>Feed</h3><p id="feed-state" data-tone="idle">Idle</p></article>
      </div>
    </section>

    <section class="dashboard">
      <article class="panel feed-panel">
        <h2>Simulated feed</h2>
        <p id="empty-feed" class="muted">No post is visible yet.</p>
        <div id="post-card" class="post-card" hidden>
          <div class="photo-placeholder" aria-label="Abstract photo placeholder"></div>
          <div>
            <strong id="post-caption">A fast-feeling post</strong>
            <p id="post-status" class="status-chip">Pending</p>
          </div>
        </div>
      </article>

      <article class="panel">
        <h2>Current run</h2>
        <div class="metrics">
          <div class="metric-card"><span>Perceived result</span><strong id="perceived-metric">Not run</strong></div>
          <div class="metric-card"><span>Server acknowledgement</span><strong id="ack-metric">Not run</strong></div>
          <div class="metric-card"><span>Publication completion</span><strong id="publication-metric">Not run</strong></div>
          <div class="metric-card"><span>Duplicates prevented</span><strong id="duplicates-metric">0</strong></div>
        </div>
      </article>
    </section>

    <section class="panel" aria-labelledby="comparison-title">
      <h2 id="comparison-title">Latest measured comparison</h2>
      <p class="muted">Elapsed time is measured inside this local simulation. It is not an Instagram production benchmark.</p>
      <div class="table-scroll">
        <table class="comparison-table">
          <thead>
            <tr><th>Design</th><th>Perceived result</th><th>Server acknowledgement</th><th>Publication completion</th></tr>
          </thead>
          <tbody>
            <tr><th>Blocking</th><td id="blocking-perceived">Not run</td><td id="blocking-ack">Not run</td><td id="blocking-publication">Not run</td></tr>
            <tr><th>Responsive</th><td id="responsive-perceived">Not run</td><td id="responsive-ack">Not run</td><td id="responsive-publication">Not run</td></tr>
          </tbody>
        </table>
      </div>
    </section>

    <section class="panel" aria-labelledby="log-title">
      <h2 id="log-title">Event log</h2>
      <ol id="event-log" class="event-log"></ol>
    </section>

    <div id="secret-mission"></div>
  </main>
</body>
</html>
Style the simulator surface

The HTML now contains every part of the lab, but its hierarchy is difficult to scan. The stylesheet turns those elements into distinct controls, lanes, feed areas, and measurement panels.

  • Select style.css in the Explorer.
  • Add the page theme and typography by pasting this code:
:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #0b1020;
  color: #f7f8ff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(circle at top left, rgba(236, 72, 153, 0.18), transparent 34rem),
    radial-gradient(circle at top right, rgba(59, 130, 246, 0.16), transparent 30rem),
    #0b1020;
}

button, input { font: inherit; }
.page-shell { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 72px; }
.hero { margin-bottom: 24px; }
.eyebrow { color: #f9a8d4; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 760px; margin-bottom: 12px; font-size: clamp(2.3rem, 7vw, 5rem); line-height: 0.98; }
.lede { max-width: 760px; color: #cbd5e1; font-size: 1.15rem; }
.disclaimer { max-width: 760px; padding-left: 12px; border-left: 3px solid #f59e0b; color: #fde68a; }

What does this CSS control?

  • The root styles establish the dark color palette and shared font stack.
  • The body gradients separate the page from a flat background.
  • The page-shell width keeps the simulator readable on wide screens.
  • The hero styles make the evidence boundary visible beside the project introduction.
  • Save style.css.
  • Return to Safari.
  • Reload the local page.

You should see a dark page with colored background glows, a large title, and a yellow disclaimer border.

Stylesheet not loading?

Confirm that style.css sits beside index.html. Check that the stylesheet link in index.html uses href="style.css".

Help me connect my stylesheet.

Panels group related information without hiding the whole workflow. Control styling also gives the three buttons and input field consistent visual weight.

  • Switch back to style.css.
  • Add the panel and control styles below the existing code by pasting this code:
.panel {
  margin-bottom: 20px;
  padding: 22px;
  border: 1px solid rgba(148, 163, 184, 0.2);
  border-radius: 18px;
  background: rgba(15, 23, 42, 0.82);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.2);
  backdrop-filter: blur(14px);
}

.controls { display: grid; gap: 12px; }
.controls input[type="text"], .controls input[type="number"] {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid #475569;
  border-radius: 10px;
  background: #111827;
  color: #f8fafc;
}

.check-row { display: flex; align-items: center; gap: 8px; color: #cbd5e1; }
.button-row { display: flex; flex-wrap: wrap; gap: 10px; }
button { padding: 11px 15px; border: 0; border-radius: 10px; background: linear-gradient(135deg, #db2777, #7c3aed); color: white; cursor: pointer; font-weight: 800; }
button.secondary { background: #334155; }
button:disabled { cursor: wait; opacity: 0.5; }

How do the panels help?

  • The panel boundary turns each part of the simulator into a clear visual group.
  • The controls use a grid so labels and fields keep predictable spacing.
  • Disabled controls become dimmer during future simulated work.
  • Save style.css.
  • Return to Safari.
  • Reload the local page.

You should see rounded panels, a full-width caption field, colored run buttons, and a darker reset control.

Panels still look like plain text?

Confirm that the selector begins with .panel and includes the leading period. Check that the HTML sections use class="panel".

Help me match my panel selector.

The remaining styles make state changes readable at a glance. They also keep the dashboard and lane layout usable when the browser window becomes narrow.

  • Switch back to style.css.
  • Add the lane, dashboard, metric, table, log, and responsive styles below the existing code by pasting this code:
.lanes { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
.lane { min-height: 132px; padding: 16px; border: 1px solid #334155; border-radius: 14px; background: #111827; }
.lane p { min-height: 52px; margin-bottom: 0; padding: 10px; border-radius: 9px; background: #1e293b; color: #cbd5e1; }
.lane p[data-tone="active"] { background: #172554; color: #bfdbfe; }
.lane p[data-tone="success"] { background: #052e16; color: #bbf7d0; }
.lane p[data-tone="danger"] { background: #450a0a; color: #fecaca; }

.dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.feed-panel { min-height: 260px; }
.post-card { display: grid; grid-template-columns: 140px 1fr; gap: 16px; align-items: center; }
.photo-placeholder { aspect-ratio: 1; border-radius: 16px; background: linear-gradient(145deg, rgba(255,255,255,0.18), transparent), linear-gradient(135deg, #f97316, #db2777 45%, #4f46e5); }
.status-chip { display: inline-block; margin-top: 10px; padding: 5px 9px; border-radius: 999px; background: #312e81; color: #e0e7ff; font-size: 0.84rem; font-weight: 800; }
.metrics { display: grid; gap: 10px; }
.metric-card { display: flex; justify-content: space-between; gap: 16px; padding: 13px; border-radius: 10px; background: #111827; }
.metric-card span, .muted { color: #94a3b8; }

.table-scroll { overflow-x: auto; }
.comparison-table { width: 100%; border-collapse: collapse; text-align: left; }
.comparison-table th, .comparison-table td { padding: 12px; border-bottom: 1px solid #334155; white-space: nowrap; }
.comparison-table thead th { color: #f9a8d4; }
.comparison-table tbody th { color: #e2e8f0; }

.event-log { display: grid; gap: 8px; margin-bottom: 0; padding-left: 24px; }
.event-log li { padding: 10px 12px; border-radius: 9px; background: #111827; color: #dbeafe; }

@media (max-width: 800px) {
  .lanes, .dashboard { grid-template-columns: 1fr; }
}

How will state changes appear?

  • Each lane has idle, active, success, and danger colors for future state changes.
  • The dashboard places the feed beside the current metrics on wide screens.
  • The table keeps its columns readable through horizontal scrolling.
  • The responsive rule stacks the lanes and dashboard on a narrow screen.
  • Save style.css.
  • Return to Safari.
  • Reload the local page.

You should see five lane cards in one row, a two-column dashboard, four dark metric cards, a formatted comparison table, and an empty event-log panel.

Layout stacked on a wide screen?

Confirm that the closing brace for the @media rule appears at the end of style.css. A missing brace can apply the narrow-screen layout everywhere.

Help me debug the simulator layout.

✔️ Awesome, I've got everything!

Your complete visual design is ready. Save style.css before initializing the shell behavior.

ⓧ I'd like to double check the full code

:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #0b1020;
  color: #f7f8ff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(circle at top left, rgba(236, 72, 153, 0.18), transparent 34rem),
    radial-gradient(circle at top right, rgba(59, 130, 246, 0.16), transparent 30rem),
    #0b1020;
}

button, input { font: inherit; }
.page-shell { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 72px; }
.hero { margin-bottom: 24px; }
.eyebrow { color: #f9a8d4; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 760px; margin-bottom: 12px; font-size: clamp(2.3rem, 7vw, 5rem); line-height: 0.98; }
.lede { max-width: 760px; color: #cbd5e1; font-size: 1.15rem; }
.disclaimer { max-width: 760px; padding-left: 12px; border-left: 3px solid #f59e0b; color: #fde68a; }

.panel {
  margin-bottom: 20px;
  padding: 22px;
  border: 1px solid rgba(148, 163, 184, 0.2);
  border-radius: 18px;
  background: rgba(15, 23, 42, 0.82);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.2);
  backdrop-filter: blur(14px);
}

.controls { display: grid; gap: 12px; }
.controls input[type="text"], .controls input[type="number"] {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid #475569;
  border-radius: 10px;
  background: #111827;
  color: #f8fafc;
}

.check-row { display: flex; align-items: center; gap: 8px; color: #cbd5e1; }
.button-row { display: flex; flex-wrap: wrap; gap: 10px; }
button { padding: 11px 15px; border: 0; border-radius: 10px; background: linear-gradient(135deg, #db2777, #7c3aed); color: white; cursor: pointer; font-weight: 800; }
button.secondary { background: #334155; }
button:disabled { cursor: wait; opacity: 0.5; }

.lanes { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
.lane { min-height: 132px; padding: 16px; border: 1px solid #334155; border-radius: 14px; background: #111827; }
.lane p { min-height: 52px; margin-bottom: 0; padding: 10px; border-radius: 9px; background: #1e293b; color: #cbd5e1; }
.lane p[data-tone="active"] { background: #172554; color: #bfdbfe; }
.lane p[data-tone="success"] { background: #052e16; color: #bbf7d0; }
.lane p[data-tone="danger"] { background: #450a0a; color: #fecaca; }

.dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.feed-panel { min-height: 260px; }
.post-card { display: grid; grid-template-columns: 140px 1fr; gap: 16px; align-items: center; }
.photo-placeholder { aspect-ratio: 1; border-radius: 16px; background: linear-gradient(145deg, rgba(255,255,255,0.18), transparent), linear-gradient(135deg, #f97316, #db2777 45%, #4f46e5); }
.status-chip { display: inline-block; margin-top: 10px; padding: 5px 9px; border-radius: 999px; background: #312e81; color: #e0e7ff; font-size: 0.84rem; font-weight: 800; }
.metrics { display: grid; gap: 10px; }
.metric-card { display: flex; justify-content: space-between; gap: 16px; padding: 13px; border-radius: 10px; background: #111827; }
.metric-card span, .muted { color: #94a3b8; }

.table-scroll { overflow-x: auto; }
.comparison-table { width: 100%; border-collapse: collapse; text-align: left; }
.comparison-table th, .comparison-table td { padding: 12px; border-bottom: 1px solid #334155; white-space: nowrap; }
.comparison-table thead th { color: #f9a8d4; }
.comparison-table tbody th { color: #e2e8f0; }

.event-log { display: grid; gap: 8px; margin-bottom: 0; padding-left: 24px; }
.event-log li { padding: 10px 12px; border-radius: 9px; background: #111827; color: #dbeafe; }

@media (max-width: 800px) {
  .lanes, .dashboard { grid-template-columns: 1fr; }
}
Initialize the simulator state

The shell needs shared timing and rendering helpers before either posting flow can run. This step creates that foundation while leaving the blocking and responsive flow logic for the next steps.

Why use a local simulator?

A local browser model keeps the focus on the critical path and visible state transitions. It avoids adding deployment work that would hide the timing lesson behind infrastructure setup.

  • Select app.js in the Explorer.
  • Add the timing configuration and shared state by pasting this code:
const $ = (id) => document.getElementById(id);
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const elapsedMilliseconds = (startedAt) => Math.round(performance.now() - startedAt);
const formatMilliseconds = (value) => value === null ? "Not run" : `${value} ms`;

const durations = {
  upload: 550,
  persist: 450,
  accept: 300,
  process: 700,
  fanout: 600,
  retryDelay: 500
};

const state = {
  running: false,
  postSequence: 0,
  publishedKeys: new Set(),
  duplicatesPrevented: 0,
  results: { blocking: null, responsive: null }
};

What does this JavaScript establish?

  • The $ helper retrieves page elements by their IDs.
  • The timing helpers pause simulated work and measure elapsed browser time.
  • The durations values configure this local simulation.
  • The state object keeps run status, result history, publication keys, and duplicate counts together.

The next helpers change what the learner sees without duplicating DOM update logic in every flow.

  • Add the lane, log, and post helpers below the existing code by pasting this code:
function setLane(lane, message, tone = "idle") {
  const element = $(`${lane}-state`);
  element.textContent = message;
  element.dataset.tone = tone;
}

function logEvent(message) {
  const item = document.createElement("li");
  item.textContent = message;
  $("event-log").prepend(item);
}

function showPost(caption, status) {
  $("post-card").hidden = false;
  $("empty-feed").hidden = true;
  $("post-caption").textContent = caption;
  $("post-status").textContent = status;
}

function hidePost() {
  $("post-card").hidden = true;
  $("empty-feed").hidden = false;
}

How do these helpers change the page?

  • The setLane() helper updates a lane message and its visual tone.
  • The logEvent() helper places the newest event at the top of the log.
  • The post helpers switch between the empty-feed message and the simulated post card.
  • Save app.js.
  • Return to Safari.
  • Reload the local page.

The complete shell should still render without a browser error page. The controls and five lanes should remain visible.

Page stopped rendering?

Check the closing braces in durations and state. Confirm that each helper function ends with one closing brace.

Help me find the JavaScript syntax problem.

Comparison rendering keeps the two design rows independent. Current-run rendering updates the four cards without changing the stored comparison.

  • Switch back to app.js.
  • Add the control and metric-rendering helpers below the existing code by pasting this code:
function setControlsDisabled(disabled) {
  ["run-blocking", "run-async", "reset-simulator"].forEach((id) => {
    $(id).disabled = disabled;
  });
}

function renderComparison() {
  ["blocking", "responsive"].forEach((design) => {
    const result = state.results[design];
    $(`${design}-perceived`).textContent = formatMilliseconds(result?.perceived ?? null);
    $(`${design}-ack`).textContent = formatMilliseconds(result?.acknowledgement ?? null);
    $(`${design}-publication`).textContent = formatMilliseconds(result?.publication ?? null);
  });
}

function showCurrentMetrics(result) {
  $("perceived-metric").textContent = formatMilliseconds(result.perceived);
  $("ack-metric").textContent = formatMilliseconds(result.acknowledgement);
  $("publication-metric").textContent = formatMilliseconds(result.publication);
  $("duplicates-metric").textContent = String(state.duplicatesPrevented);
}

Why separate current and stored results?

The current cards describe the active run. The comparison table preserves the newest Blocking and Responsive results independently.

The control helper can lock all three buttons while future simulated work is in progress.

  • Save app.js.
  • Return to Safari.
  • Reload the local page.

You should still see three untouched timing values, a duplicate count of 0, and two untouched comparison rows.

Metrics showing unexpected text?

Confirm that the metric IDs in app.js match the IDs in index.html. Check the spelling of acknowledgement in the result lookup.

Help me match the metric IDs.

The reset functions create a repeatable neutral baseline. That baseline matters because each later flow must begin with the same empty feed, idle lanes, and cleared current measurements.

  • Switch back to app.js.
  • Add the reset functions and reset-button listener below the existing code by pasting this code:
function resetCurrentRun() {
  state.running = false;
  state.publishedKeys.clear();
  state.duplicatesPrevented = 0;
  ["client", "api", "queue", "worker", "feed"].forEach((lane) => setLane(lane, "Idle"));
  hidePost();
  $("event-log").textContent = "";
  showCurrentMetrics({ perceived: null, acknowledgement: null, publication: null });
  setControlsDisabled(false);
}

function resetSimulator() {
  resetCurrentRun();
  state.results = { blocking: null, responsive: null };
  renderComparison();
}

$("reset-simulator").addEventListener("click", resetSimulator);

resetSimulator();

What happens during a reset?

  • The current-run reset clears publication keys, duplicate counts, the post card, and the event log.
  • Each system lane returns to Idle.
  • The full reset also clears the stored Blocking and Responsive comparison results.
  • The final call initializes the same neutral state whenever the page loads.
  • Save app.js.
  • Return to Safari.

Before you reload, which values should prove that the simulator has returned to its neutral baseline?

  • Reload the local page.
  • Click Reset all results.

You should see all five lanes read Idle. The three timing cards and every comparison timing cell should read Not run.

You should also see a duplicate count of 0, an empty event log, and the message No post is visible yet.

Reset button not restoring the shell?

  • Confirm that resetSimulator() appears after both reset function declarations.
  • Confirm that the listener uses the reset-simulator ID from index.html.
  • Check that app.js is saved beside index.html.

Help me debug the simulator reset.

✔️ Awesome, I've got everything!

Your shell helpers are initialized. Save app.js and keep the local Safari page available for the next flow.

ⓧ I'd like to double check the full code

const $ = (id) => document.getElementById(id);
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const elapsedMilliseconds = (startedAt) => Math.round(performance.now() - startedAt);
const formatMilliseconds = (value) => value === null ? "Not run" : `${value} ms`;

const durations = {
  upload: 550,
  persist: 450,
  accept: 300,
  process: 700,
  fanout: 600,
  retryDelay: 500
};

const state = {
  running: false,
  postSequence: 0,
  publishedKeys: new Set(),
  duplicatesPrevented: 0,
  results: { blocking: null, responsive: null }
};

function setLane(lane, message, tone = "idle") {
  const element = $(`${lane}-state`);
  element.textContent = message;
  element.dataset.tone = tone;
}

function logEvent(message) {
  const item = document.createElement("li");
  item.textContent = message;
  $("event-log").prepend(item);
}

function showPost(caption, status) {
  $("post-card").hidden = false;
  $("empty-feed").hidden = true;
  $("post-caption").textContent = caption;
  $("post-status").textContent = status;
}

function hidePost() {
  $("post-card").hidden = true;
  $("empty-feed").hidden = false;
}

function setControlsDisabled(disabled) {
  ["run-blocking", "run-async", "reset-simulator"].forEach((id) => {
    $(id).disabled = disabled;
  });
}

function renderComparison() {
  ["blocking", "responsive"].forEach((design) => {
    const result = state.results[design];
    $(`${design}-perceived`).textContent = formatMilliseconds(result?.perceived ?? null);
    $(`${design}-ack`).textContent = formatMilliseconds(result?.acknowledgement ?? null);
    $(`${design}-publication`).textContent = formatMilliseconds(result?.publication ?? null);
  });
}

function showCurrentMetrics(result) {
  $("perceived-metric").textContent = formatMilliseconds(result.perceived);
  $("ack-metric").textContent = formatMilliseconds(result.acknowledgement);
  $("publication-metric").textContent = formatMilliseconds(result.publication);
  $("duplicates-metric").textContent = String(state.duplicatesPrevented);
}

function resetCurrentRun() {
  state.running = false;
  state.publishedKeys.clear();
  state.duplicatesPrevented = 0;
  ["client", "api", "queue", "worker", "feed"].forEach((lane) => setLane(lane, "Idle"));
  hidePost();
  $("event-log").textContent = "";
  showCurrentMetrics({ perceived: null, acknowledgement: null, publication: null });
  setControlsDisabled(false);
}

function resetSimulator() {
  resetCurrentRun();
  state.results = { blocking: null, responsive: null };
  renderComparison();
}

$("reset-simulator").addEventListener("click", resetSimulator);

resetSimulator();

That is the visual baseline locked in. Your local simulator now exposes every stage and measurement area needed for a fair latency comparison.

Your simulator shell is ready. Next, you will place the entire posting pipeline on the user-facing path and feel the cost of waiting for every stage.

Measure the Blocking Flow

Your simulator shell makes each posting stage visible. The controls now need behavior that exposes the delay created by a blocking design.

A fully synchronous path keeps upload, persistence, processing, and feed distribution on the user-facing critical path. This step measures the pause before the post becomes visible.

In this step, get ready to:
  • Start one timer before the simulated posting work begins.
  • Run every blocking stage in sequence while the post remains hidden.
  • Store one elapsed time across all three Blocking measurements.
Start the blocking timer

Browser timing gives each run a single start point. The existing elapsedMilliseconds() helper later measures every milestone from that point.

  • In the Explorer sidebar in Visual Studio Code, select app.js.
  • Find the event-listener lines near the bottom of app.js.
  • Insert the following function directly above the event-listener lines:
async function runBlocking() {
  if (state.running) return;
  resetCurrentRun();
  state.running = true;
  setControlsDisabled(true);

  const startedAt = performance.now();
  const postId = ++state.postSequence;
  const caption = $("caption").value || `Post ${postId}`;

  setLane("client", "Waiting for the whole request", "active");
  setLane("api", "Receiving media", "active");
  logEvent("Client sends media and waits.");
  await wait(durations.upload);

  state.running = false;
  setControlsDisabled(false);
}

What Does This Code Do?

  • The state.running guard prevents overlapping simulator runs.
  • The startedAt value captures the beginning of the request with performance.now().
  • The postId gives each simulated post a sequence number.
  • The first wait keeps the client blocked for the configured upload duration.
  • Save app.js.
  • Switch back to Safari.
  • Click Run blocking flow.

You should see the Client lane change to Waiting for the whole request. The API lane changes to Receiving media while the first event enters the log.

Does the Button Leave Every Lane Idle?

Confirm that runBlocking sits above the event-listener lines. Check that the function name matches the existing blocking button listener exactly.

If the button still does not update the lanes, help me trace why runBlocking is not responding.

Put every stage on the critical path

The blocking design completes each stage before starting the next one. The client stays waiting throughout this sequence.

  • In runBlocking(), find this section after the upload wait:
  await wait(durations.upload);

  state.running = false;

What Are You Locating?

This section currently restores the controls immediately after the upload delay. The remaining blocking stages belong before that restoration.

  • Replace that section with the sequential persistence, processing, and distribution stages below:
  await wait(durations.upload);

  setLane("api", "Persisting post metadata", "active");
  logEvent("API persists the post before it can reply.");
  await wait(durations.persist);

  setLane("worker", "Processing media inline", "active");
  logEvent("Slow media work remains on the request path.");
  await wait(durations.process);

  setLane("feed", "Distributing the post inline", "active");
  logEvent("Feed distribution also blocks the response.");
  await wait(durations.fanout);

  state.running = false;

Why Do These Waits Matter?

  • Each await prevents the next stage from starting until the current delay finishes.
  • The API performs persistence before the Worker begins media processing.
  • Feed distribution finishes last while the client remains blocked.
  • Save app.js.

This check takes about 2.3 seconds because the simulator waits through all four configured delays.

Before you click the button, which do you expect to happen first: the post becoming visible or the Feed lane finishing its work?

  • Switch back to Safari.
  • Click Run blocking flow.

You should see the API, Worker, and Feed lanes activate one after another. The feed stays empty throughout the run because publication still has no earlier boundary.

That Pause Is the Point

The client cannot show a successful result while every stage remains on the same request path. This visible wait is the limitation that asynchronous acknowledgement fixes.

Do the Lanes Stop Too Early?

Check that every new stage appears before state.running = false. Confirm that each stage keeps its matching await wait(...) line.

If one lane never activates, help me compare my blocking stage sequence with the intended flow.

Publish and store one elapsed result

Publication is the first successful visible state in this design. The same elapsed value therefore represents the perceived result, server acknowledgement, and publication completion.

  • In runBlocking(), find the final Feed wait followed by the control restoration:
  await wait(durations.fanout);

  state.running = false;

Where Does Publication Belong?

The publication result belongs after the Feed delay. This placement keeps the post hidden until every blocking stage has finished.

  • Replace that section with the publication, measurement, rendering, and completion code below:
  await wait(durations.fanout);

  showPost(caption, "Published");
  const publication = elapsedMilliseconds(startedAt);
  const result = { perceived: publication, acknowledgement: publication, publication };
  state.results.blocking = result;
  showCurrentMetrics(result);
  renderComparison();

  setLane("client", "Post finally visible", "success");
  setLane("api", "Response returned", "success");
  setLane("queue", "Unused", "idle");
  setLane("worker", "Complete", "success");
  setLane("feed", "One post visible", "success");
  logEvent("The blocking design reaches all three measured milestones together.");

  state.running = false;

What Does the Measurement Prove?

  • The showPost() call makes the card visible only after feed distribution finishes.
  • The publication value measures elapsed browser time from the original request start.
  • The result object assigns that one value to all three milestones.
  • The renderComparison() call preserves the measurement in the Blocking row.
  • Save app.js.

Before you run the completed flow, do you expect any Blocking measurement to finish earlier than the others?

  • Switch back to Safari.
  • Click Run blocking flow.

The post card should remain absent until the Feed lane completes. You should then see one Published post with successful Client, API, Worker, and Feed lanes.

The Blocking row should show the same measured millisecond value for perceived result, server acknowledgement, and publication completion. The Responsive row should still read Not run.

Do the Blocking Values Differ?

Confirm that perceived, acknowledgement, and publication all receive the same publication value. Check that the elapsed measurement appears after showPost().

If the table remains unchanged, help me debug the blocking result rendering.

✔️ Awesome, I've got everything!

Your blocking flow now keeps every stage on one request path. Confirm that app.js is saved before moving on.

ⓧ I'd like to double check the full code

const $ = (id) => document.getElementById(id);
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const elapsedMilliseconds = (startedAt) => Math.round(performance.now() - startedAt);
const formatMilliseconds = (value) => value === null ? "Not run" : `${value} ms`;

const durations = {
  upload: 550,
  persist: 450,
  accept: 300,
  process: 700,
  fanout: 600,
  retryDelay: 500
};

const state = {
  running: false,
  postSequence: 0,
  publishedKeys: new Set(),
  duplicatesPrevented: 0,
  results: { blocking: null, responsive: null }
};

function setLane(lane, message, tone = "idle") {
  const element = $(`${lane}-state`);
  element.textContent = message;
  element.dataset.tone = tone;
}

function logEvent(message) {
  const item = document.createElement("li");
  item.textContent = message;
  $("event-log").prepend(item);
}

function showPost(caption, status) {
  $("post-card").hidden = false;
  $("empty-feed").hidden = true;
  $("post-caption").textContent = caption;
  $("post-status").textContent = status;
}

function hidePost() {
  $("post-card").hidden = true;
  $("empty-feed").hidden = false;
}

function setControlsDisabled(disabled) {
  ["run-blocking", "run-async", "reset-simulator"].forEach((id) => {
    $(id).disabled = disabled;
  });
}

function renderComparison() {
  ["blocking", "responsive"].forEach((design) => {
    const result = state.results[design];
    $(`${design}-perceived`).textContent = formatMilliseconds(result?.perceived ?? null);
    $(`${design}-ack`).textContent = formatMilliseconds(result?.acknowledgement ?? null);
    $(`${design}-publication`).textContent = formatMilliseconds(result?.publication ?? null);
  });
}

function showCurrentMetrics(result) {
  $("perceived-metric").textContent = formatMilliseconds(result.perceived);
  $("ack-metric").textContent = formatMilliseconds(result.acknowledgement);
  $("publication-metric").textContent = formatMilliseconds(result.publication);
  $("duplicates-metric").textContent = String(state.duplicatesPrevented);
}

function resetCurrentRun() {
  state.running = false;
  state.publishedKeys.clear();
  state.duplicatesPrevented = 0;
  ["client", "api", "queue", "worker", "feed"].forEach((lane) => setLane(lane, "Idle"));
  hidePost();
  $("event-log").textContent = "";
  showCurrentMetrics({ perceived: null, acknowledgement: null, publication: null });
  setControlsDisabled(false);
}

function resetSimulator() {
  resetCurrentRun();
  state.results = { blocking: null, responsive: null };
  renderComparison();
}

async function runBlocking() {
  if (state.running) return;
  resetCurrentRun();
  state.running = true;
  setControlsDisabled(true);

  const startedAt = performance.now();
  const postId = ++state.postSequence;
  const caption = $("caption").value || `Post ${postId}`;

  setLane("client", "Waiting for the whole request", "active");
  setLane("api", "Receiving media", "active");
  logEvent("Client sends media and waits.");
  await wait(durations.upload);

  setLane("api", "Persisting post metadata", "active");
  logEvent("API persists the post before it can reply.");
  await wait(durations.persist);

  setLane("worker", "Processing media inline", "active");
  logEvent("Slow media work remains on the request path.");
  await wait(durations.process);

  setLane("feed", "Distributing the post inline", "active");
  logEvent("Feed distribution also blocks the response.");
  await wait(durations.fanout);

  showPost(caption, "Published");
  const publication = elapsedMilliseconds(startedAt);
  const result = { perceived: publication, acknowledgement: publication, publication };
  state.results.blocking = result;
  showCurrentMetrics(result);
  renderComparison();

  setLane("client", "Post finally visible", "success");
  setLane("api", "Response returned", "success");
  setLane("queue", "Unused", "idle");
  setLane("worker", "Complete", "success");
  setLane("feed", "One post visible", "success");
  logEvent("The blocking design reaches all three measured milestones together.");

  state.running = false;
  setControlsDisabled(false);
}

$("run-blocking").addEventListener("click", runBlocking);
$("reset-simulator").addEventListener("click", resetSimulator);

resetSimulator();

Compare this reference with your saved app.js. The blocking function should appear after the reset helpers and before the event listeners.

Your simulator now quantifies the cost of keeping every stage on the critical path. Next, you will move slow work behind an early acknowledgement and watch the perceived delay shrink.

Queue Work Behind an Acknowledgement

The blocking flow proved that upload, persistence, processing, and feed distribution all sit on the critical path. The client waits for every stage before it can show a result.

This step separates perceived latency from acknowledgement latency. A pending card appears immediately while a Queue and Worker finish publication in the background.

In this step, get ready to:
  • Render a pending post immediately and measure its perceived latency.
  • Acknowledge accepted work before media processing finishes.
  • Publish the queued post through the worker and compare all three latency boundaries.
Render a pending post immediately

Perceived latency measures how long the client waits before showing useful feedback. The responsive flow records that boundary as soon as the pending card becomes visible.

  • Switch back to app.js in Visual Studio Code.
  • Place this first responsive-flow slice below runBlocking():
async function runAsync() {
  if (state.running) return;
  resetCurrentRun();
  state.running = true;
  setControlsDisabled(true);

  const startedAt = performance.now();
  const postId = ++state.postSequence;
  const job = {
    requestKey: `post-request-${postId}`,
    caption: $("caption").value || `Post ${postId}`,
    startedAt,
    publicationLatency: null
  };
  const shouldLoseAcknowledgement = $("fail-first").checked;

  showPost(job.caption, "Pending");
  const perceived = elapsedMilliseconds(startedAt);
  $("perceived-metric").textContent = formatMilliseconds(perceived);
  setLane("client", "Pending post visible immediately", "success");
  setLane("api", "Accepting request", "active");
  logEvent(`Client renders a pending card for ${job.requestKey}.`);

  state.running = false;
  setControlsDisabled(false);
}

What does this code do?

  • The job object keeps one request key with its caption and timing data.
  • The call to showPost() gives the client a visible pending state before any simulated API delay.
  • The first call to elapsedMilliseconds() captures how quickly that useful feedback reaches the screen.

The button still needs to call the new function. The event-listener section at the bottom of app.js connects each control to its flow.

  • Update the listener section at the bottom of app.js so it matches this code:
$("run-blocking").addEventListener("click", runBlocking);
$("run-async").addEventListener("click", runAsync);
$("reset-simulator").addEventListener("click", resetSimulator);

resetSimulator();

How is the button connected?

The listener calls runAsync() whenever the responsive-flow button receives a click. The existing listeners keep the blocking and reset controls working.

  • Save app.js.
  • Return to Safari and reload the simulator.
  • Clear the Fail first worker acknowledgement checkbox.
  • Click Run async flow.

You should see a pending post immediately. The Client lane reports that the post is visible while the API lane reports that it is accepting the request.

That instant card proves the client can respond before background publication finishes.

Pending card not appearing?

  • Check that runAsync() sits outside runBlocking().
  • Check that the responsive listener uses the exact run-async element ID.

Help me debug why runAsync() does not render the pending post.

Move accepted work through the worker

An acknowledgement marks the point where the API has accepted the request into the background pipeline. Publication remains a separate milestone because processing and feed distribution still need time.

  • Add the publication boundary below runBlocking() by copying this function:
async function publishOnce(job) {
  if (state.publishedKeys.has(job.requestKey)) {
    state.duplicatesPrevented += 1;
    $("duplicates-metric").textContent = String(state.duplicatesPrevented);
    setLane("feed", "Duplicate publish prevented", "success");
    logEvent(`Duplicate publish prevented for ${job.requestKey}.`);
    return false;
  }

  state.publishedKeys.add(job.requestKey);
  setLane("feed", "Distributing accepted post", "active");
  await wait(durations.fanout);
  showPost(job.caption, "Published");
  job.publicationLatency = elapsedMilliseconds(job.startedAt);
  $("publication-metric").textContent = formatMilliseconds(job.publicationLatency);
  setLane("feed", "One post visible", "success");
  logEvent(`Published exactly one feed item for ${job.requestKey}.`);
  return true;
}

What does publication record?

  • The request key enters state.publishedKeys when the publication boundary accepts it.
  • The fan-out delay represents work that continues after the API acknowledgement.
  • The publicationLatency value captures the first moment the post becomes published.
  • Place the worker orchestration below publishOnce() by copying this function:
async function processJob(job, shouldLoseAcknowledgement) {
  setLane("queue", "Worker claimed one job", "active");
  setLane("worker", "Attempt 1: processing media", "active");
  logEvent(`Worker starts attempt 1 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);

  if (!shouldLoseAcknowledgement) {
    setLane("queue", "Job acknowledged", "success");
    setLane("worker", "Complete", "success");
    return;
  }

  setLane("worker", "Acknowledgement lost after publish", "danger");
  setLane("queue", "Same job scheduled for retry", "active");
  logEvent("Acknowledgement lost after publish. The queue will retry the same request key.");
  await wait(durations.retryDelay);

  setLane("worker", "Attempt 2: same request key", "active");
  logEvent(`Worker starts attempt 2 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);
  setLane("queue", "Retry acknowledged", "success");
  setLane("worker", "Complete after safe retry", "success");
}

How does the worker fit in?

The worker claims the queued job before waiting for media processing. It passes the same job to publishOnce() when processing finishes.

With the failure option cleared, the queue acknowledges the completed job after its first worker attempt.

The first responsive slice stops after rendering the pending card. Extend its final section so the API records acceptance before handing the job to the worker.

  • Find these final lines inside runAsync():
  state.running = false;
  setControlsDisabled(false);
}

What are you replacing?

These lines currently finish the responsive flow immediately after the pending card appears. The replacement keeps the run active through acknowledgement and publication.

  • Replace those final lines with this completed flow:
  await wait(durations.accept);
  const acknowledgement = elapsedMilliseconds(startedAt);
  $("ack-metric").textContent = formatMilliseconds(acknowledgement);
  setLane("api", "Accepted and queued", "success");
  setLane("queue", "One job waiting", "active");
  $("post-status").textContent = "Accepted";
  logEvent("API acknowledges accepted work before media processing finishes.");

  await processJob(job, shouldLoseAcknowledgement);
  const result = { perceived, acknowledgement, publication: job.publicationLatency };
  state.results.responsive = result;
  showCurrentMetrics(result);
  renderComparison();
  setLane("client", "Published state reconciled", "success");
  logEvent("Client preserves the first-publication measurement after any safe retry.");

  state.running = false;
  setControlsDisabled(false);
}

Where are the three boundaries?

  • The pending card sets perceived before the API delay begins.
  • The accepted request sets acknowledgement before worker processing begins.
  • The first successful feed update sets publication after background work finishes.
  • Save app.js.
  • Return to Safari and reload the simulator.

Before you run the completed flow, which milestone do you expect to happen first?

  • Clear the Fail first worker acknowledgement checkbox.
  • Click Run async flow.

Give the simulation about two seconds to finish. You should see the card move from Pending to Accepted before reaching Published.

Flow stopping before publication?

  • Check that publishOnce() appears before processJob() in app.js.
  • Check that runAsync() awaits processJob(job, shouldLoseAcknowledgement).

Help me trace why my responsive flow stops before the post reaches Published.

Compare the three latency boundaries

The comparison table preserves the latest blocking result while the responsive flow runs. This puts the user-facing delay beside the time required for complete publication.

Before the final check, which responsive value do you expect to be shortest?

  • Confirm that the Blocking row still contains the previous run's measurements.
  • Clear the Fail first worker acknowledgement checkbox.
  • Click Run async flow.

You should see the shortest responsive value under Perceived result. The server acknowledgement should follow before publication completion.

You have separated immediate feedback from durable completion. The client now feels responsive while the slow pipeline continues behind the acknowledgement.

✔️ Awesome, I've got everything!

Great. Double-check that you saved app.js after the successful responsive run.

ⓧ I'd like to double check the full code

const $ = (id) => document.getElementById(id);
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const elapsedMilliseconds = (startedAt) => Math.round(performance.now() - startedAt);
const formatMilliseconds = (value) => value === null ? "Not run" : `${value} ms`;

const durations = {
  upload: 550,
  persist: 450,
  accept: 300,
  process: 700,
  fanout: 600,
  retryDelay: 500
};

const state = {
  running: false,
  postSequence: 0,
  publishedKeys: new Set(),
  duplicatesPrevented: 0,
  results: { blocking: null, responsive: null }
};

function setLane(lane, message, tone = "idle") {
  const element = $(`${lane}-state`);
  element.textContent = message;
  element.dataset.tone = tone;
}

function logEvent(message) {
  const item = document.createElement("li");
  item.textContent = message;
  $("event-log").prepend(item);
}

function showPost(caption, status) {
  $("post-card").hidden = false;
  $("empty-feed").hidden = true;
  $("post-caption").textContent = caption;
  $("post-status").textContent = status;
}

function hidePost() {
  $("post-card").hidden = true;
  $("empty-feed").hidden = false;
}

function setControlsDisabled(disabled) {
  ["run-blocking", "run-async", "reset-simulator"].forEach((id) => {
    $(id).disabled = disabled;
  });
}

function renderComparison() {
  ["blocking", "responsive"].forEach((design) => {
    const result = state.results[design];
    $(`${design}-perceived`).textContent = formatMilliseconds(result?.perceived ?? null);
    $(`${design}-ack`).textContent = formatMilliseconds(result?.acknowledgement ?? null);
    $(`${design}-publication`).textContent = formatMilliseconds(result?.publication ?? null);
  });
}

function showCurrentMetrics(result) {
  $("perceived-metric").textContent = formatMilliseconds(result.perceived);
  $("ack-metric").textContent = formatMilliseconds(result.acknowledgement);
  $("publication-metric").textContent = formatMilliseconds(result.publication);
  $("duplicates-metric").textContent = String(state.duplicatesPrevented);
}

function resetCurrentRun() {
  state.running = false;
  state.publishedKeys.clear();
  state.duplicatesPrevented = 0;
  ["client", "api", "queue", "worker", "feed"].forEach((lane) => setLane(lane, "Idle"));
  hidePost();
  $("event-log").textContent = "";
  showCurrentMetrics({ perceived: null, acknowledgement: null, publication: null });
  setControlsDisabled(false);
}

function resetSimulator() {
  resetCurrentRun();
  state.results = { blocking: null, responsive: null };
  renderComparison();
}

async function runBlocking() {
  if (state.running) return;
  resetCurrentRun();
  state.running = true;
  setControlsDisabled(true);

  const startedAt = performance.now();
  const postId = ++state.postSequence;
  const caption = $("caption").value || `Post ${postId}`;

  setLane("client", "Waiting for the whole request", "active");
  setLane("api", "Receiving media", "active");
  logEvent("Client sends media and waits.");
  await wait(durations.upload);

  setLane("api", "Persisting post metadata", "active");
  logEvent("API persists the post before it can reply.");
  await wait(durations.persist);

  setLane("worker", "Processing media inline", "active");
  logEvent("Slow media work remains on the request path.");
  await wait(durations.process);

  setLane("feed", "Distributing the post inline", "active");
  logEvent("Feed distribution also blocks the response.");
  await wait(durations.fanout);

  showPost(caption, "Published");
  const publication = elapsedMilliseconds(startedAt);
  const result = { perceived: publication, acknowledgement: publication, publication };
  state.results.blocking = result;
  showCurrentMetrics(result);
  renderComparison();

  setLane("client", "Post finally visible", "success");
  setLane("api", "Response returned", "success");
  setLane("queue", "Unused", "idle");
  setLane("worker", "Complete", "success");
  setLane("feed", "One post visible", "success");
  logEvent("The blocking design reaches all three measured milestones together.");

  state.running = false;
  setControlsDisabled(false);
}

async function publishOnce(job) {
  if (state.publishedKeys.has(job.requestKey)) {
    state.duplicatesPrevented += 1;
    $("duplicates-metric").textContent = String(state.duplicatesPrevented);
    setLane("feed", "Duplicate publish prevented", "success");
    logEvent(`Duplicate publish prevented for ${job.requestKey}.`);
    return false;
  }

  state.publishedKeys.add(job.requestKey);
  setLane("feed", "Distributing accepted post", "active");
  await wait(durations.fanout);
  showPost(job.caption, "Published");
  job.publicationLatency = elapsedMilliseconds(job.startedAt);
  $("publication-metric").textContent = formatMilliseconds(job.publicationLatency);
  setLane("feed", "One post visible", "success");
  logEvent(`Published exactly one feed item for ${job.requestKey}.`);
  return true;
}

async function processJob(job, shouldLoseAcknowledgement) {
  setLane("queue", "Worker claimed one job", "active");
  setLane("worker", "Attempt 1: processing media", "active");
  logEvent(`Worker starts attempt 1 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);

  if (!shouldLoseAcknowledgement) {
    setLane("queue", "Job acknowledged", "success");
    setLane("worker", "Complete", "success");
    return;
  }

  setLane("worker", "Acknowledgement lost after publish", "danger");
  setLane("queue", "Same job scheduled for retry", "active");
  logEvent("Acknowledgement lost after publish. The queue will retry the same request key.");
  await wait(durations.retryDelay);

  setLane("worker", "Attempt 2: same request key", "active");
  logEvent(`Worker starts attempt 2 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);
  setLane("queue", "Retry acknowledged", "success");
  setLane("worker", "Complete after safe retry", "success");
}

async function runAsync() {
  if (state.running) return;
  resetCurrentRun();
  state.running = true;
  setControlsDisabled(true);

  const startedAt = performance.now();
  const postId = ++state.postSequence;
  const job = {
    requestKey: `post-request-${postId}`,
    caption: $("caption").value || `Post ${postId}`,
    startedAt,
    publicationLatency: null
  };
  const shouldLoseAcknowledgement = $("fail-first").checked;

  showPost(job.caption, "Pending");
  const perceived = elapsedMilliseconds(startedAt);
  $("perceived-metric").textContent = formatMilliseconds(perceived);
  setLane("client", "Pending post visible immediately", "success");
  setLane("api", "Accepting request", "active");
  logEvent(`Client renders a pending card for ${job.requestKey}.`);

  await wait(durations.accept);
  const acknowledgement = elapsedMilliseconds(startedAt);
  $("ack-metric").textContent = formatMilliseconds(acknowledgement);
  setLane("api", "Accepted and queued", "success");
  setLane("queue", "One job waiting", "active");
  $("post-status").textContent = "Accepted";
  logEvent("API acknowledges accepted work before media processing finishes.");

  await processJob(job, shouldLoseAcknowledgement);
  const result = { perceived, acknowledgement, publication: job.publicationLatency };
  state.results.responsive = result;
  showCurrentMetrics(result);
  renderComparison();
  setLane("client", "Published state reconciled", "success");
  logEvent("Client preserves the first-publication measurement after any safe retry.");

  state.running = false;
  setControlsDisabled(false);
}

$("run-blocking").addEventListener("click", runBlocking);
$("run-async").addEventListener("click", runAsync);
$("reset-simulator").addEventListener("click", resetSimulator);

resetSimulator();

Your slow work now runs behind an acknowledgement. Next up, you will make that background path safe when a worker loses its acknowledgement and retries the same job.

Retry Safely Without Duplicates

Your responsive flow already shows a pending post before background publication finishes. The simulator in Safari now exposes three distinct latency milestones.

A lost worker acknowledgement can cause the same queued job to run again.

You will use an idempotency boundary keyed by the existing request key. Repeated delivery will leave one published post.

In this step, get ready to:
  • Reject repeated publication attempts inside publishOnce().
  • Retry the same queued job after a lost worker acknowledgement.
  • Verify that the first publication measurement remains unchanged.
Reject a repeated publication

The existing publishedKeys set remembers completed request keys. A repeated key can be rejected before feed work runs again.

  • Switch back to Visual Studio Code from earlier.
  • Select app.js in the Explorer sidebar.
  • Find state.publishedKeys.add(job.requestKey); inside publishOnce().
  • Insert this guard directly above that line:
  if (state.publishedKeys.has(job.requestKey)) {
    state.duplicatesPrevented += 1;
    $("duplicates-metric").textContent = String(state.duplicatesPrevented);
    setLane("feed", "Duplicate publish prevented", "success");
    logEvent(`Duplicate publish prevented for ${job.requestKey}.`);
    return false;
  }

What does this guard do?

  • The has() check recognizes a request key that has already crossed the publication boundary.
  • The duplicate counter records each rejected publication attempt.
  • The early return false stops the second attempt before feed distribution begins.
  • The first value stored in job.publicationLatency remains untouched.
  • Save app.js.
  • Return to Safari from earlier.
  • Clear Fail first worker acknowledgement.
  • Click Run async flow.

Good progress. The normal asynchronous flow still publishes one card with a duplicate count of 0.

Does the normal flow stop early?

Check that the new guard sits above state.publishedKeys.add(job.requestKey);. The first request key must be added after the guard checks it.

Check the braces around the new if block. A misplaced closing brace can move the remaining publication code outside publishOnce().

Help me debug the duplicate guard in publishOnce().

Retry the same queued job

The first worker attempt publishes successfully. Its acknowledgement is lost afterward.

The queue responds by processing the same job again. Reusing that job preserves the original requestKey.

  • Find the complete processJob() function in app.js.
  • Replace that function with this retry-aware version:
async function processJob(job, shouldLoseAcknowledgement) {
  setLane("queue", "Worker claimed one job", "active");
  setLane("worker", "Attempt 1: processing media", "active");
  logEvent(`Worker starts attempt 1 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);

  if (!shouldLoseAcknowledgement) {
    setLane("queue", "Job acknowledged", "success");
    setLane("worker", "Complete", "success");
    return;
  }

  setLane("worker", "Acknowledgement lost after publish", "danger");
  setLane("queue", "Same job scheduled for retry", "active");
  logEvent("Acknowledgement lost after publish. The queue will retry the same request key.");
  await wait(durations.retryDelay);

  setLane("worker", "Attempt 2: same request key", "active");
  logEvent(`Worker starts attempt 2 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);
  setLane("queue", "Retry acknowledged", "success");
  setLane("worker", "Complete after safe retry", "success");
}

How does the retry work?

  • Attempt 1 processes the job before calling publishOnce().
  • A normal acknowledgement completes the job through the early return.
  • A lost acknowledgement keeps the same job available for a second attempt.
  • Attempt 2 passes the unchanged job.requestKey back to the publication boundary.
  • Save app.js.
  • Return to Safari from earlier.
  • Click Run async flow while the failure checkbox remains clear.

You should see an event for worker attempt 1. The Queue lane should finish at Job acknowledged.

Does the normal job retry?

Confirm that Fail first worker acknowledgement is clear. A clear checkbox keeps shouldLoseAcknowledgement false.

Check that the normal path contains return;. That return ends the function before the retry branch.

Help me debug the normal processJob path.

The worker now supports both outcomes. The remaining change passes the checkbox choice into that function.

  • Find the section of runAsync() immediately below the job object.
  • Replace the section through the processJob() call with this version:
  const shouldLoseAcknowledgement = $("fail-first").checked;

  showPost(job.caption, "Pending");
  const perceived = elapsedMilliseconds(startedAt);
  $("perceived-metric").textContent = formatMilliseconds(perceived);
  setLane("client", "Pending post visible immediately", "success");
  setLane("api", "Accepting request", "active");
  logEvent(`Client renders a pending card for ${job.requestKey}.`);

  await wait(durations.accept);
  const acknowledgement = elapsedMilliseconds(startedAt);
  $("ack-metric").textContent = formatMilliseconds(acknowledgement);
  setLane("api", "Accepted and queued", "success");
  setLane("queue", "One job waiting", "active");
  $("post-status").textContent = "Accepted";
  logEvent("API acknowledges accepted work before media processing finishes.");

  await processJob(job, shouldLoseAcknowledgement);

What changes in runAsync?

  • The checked property captures whether this run should lose its first worker acknowledgement.
  • The pending state still appears before API acceptance begins.
  • The API acknowledgement still occurs before worker processing begins.
  • The checkbox value now controls the normal or retry branch inside processJob().
  • Save app.js.

✔️ Awesome, I've got everything!

  • Confirm that app.js is saved.

ⓧ I'd like to double check the full code

  • Compare app.js with the complete file below:
const $ = (id) => document.getElementById(id);
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const elapsedMilliseconds = (startedAt) => Math.round(performance.now() - startedAt);
const formatMilliseconds = (value) => value === null ? "Not run" : `${value} ms`;

const durations = {
  upload: 550,
  persist: 450,
  accept: 300,
  process: 700,
  fanout: 600,
  retryDelay: 500
};

const state = {
  running: false,
  postSequence: 0,
  publishedKeys: new Set(),
  duplicatesPrevented: 0,
  results: { blocking: null, responsive: null }
};

function setLane(lane, message, tone = "idle") {
  const element = $(`${lane}-state`);
  element.textContent = message;
  element.dataset.tone = tone;
}

function logEvent(message) {
  const item = document.createElement("li");
  item.textContent = message;
  $("event-log").prepend(item);
}

function showPost(caption, status) {
  $("post-card").hidden = false;
  $("empty-feed").hidden = true;
  $("post-caption").textContent = caption;
  $("post-status").textContent = status;
}

function hidePost() {
  $("post-card").hidden = true;
  $("empty-feed").hidden = false;
}

function setControlsDisabled(disabled) {
  ["run-blocking", "run-async", "reset-simulator"].forEach((id) => {
    $(id).disabled = disabled;
  });
}

function renderComparison() {
  ["blocking", "responsive"].forEach((design) => {
    const result = state.results[design];
    $(`${design}-perceived`).textContent = formatMilliseconds(result?.perceived ?? null);
    $(`${design}-ack`).textContent = formatMilliseconds(result?.acknowledgement ?? null);
    $(`${design}-publication`).textContent = formatMilliseconds(result?.publication ?? null);
  });
}

function showCurrentMetrics(result) {
  $("perceived-metric").textContent = formatMilliseconds(result.perceived);
  $("ack-metric").textContent = formatMilliseconds(result.acknowledgement);
  $("publication-metric").textContent = formatMilliseconds(result.publication);
  $("duplicates-metric").textContent = String(state.duplicatesPrevented);
}

function resetCurrentRun() {
  state.running = false;
  state.publishedKeys.clear();
  state.duplicatesPrevented = 0;
  ["client", "api", "queue", "worker", "feed"].forEach((lane) => setLane(lane, "Idle"));
  hidePost();
  $("event-log").textContent = "";
  showCurrentMetrics({ perceived: null, acknowledgement: null, publication: null });
  setControlsDisabled(false);
}

function resetSimulator() {
  resetCurrentRun();
  state.results = { blocking: null, responsive: null };
  renderComparison();
}

async function runBlocking() {
  if (state.running) return;
  resetCurrentRun();
  state.running = true;
  setControlsDisabled(true);

  const startedAt = performance.now();
  const postId = ++state.postSequence;
  const caption = $("caption").value || `Post ${postId}`;

  setLane("client", "Waiting for the whole request", "active");
  setLane("api", "Receiving media", "active");
  logEvent("Client sends media and waits.");
  await wait(durations.upload);

  setLane("api", "Persisting post metadata", "active");
  logEvent("API persists the post before it can reply.");
  await wait(durations.persist);

  setLane("worker", "Processing media inline", "active");
  logEvent("Slow media work remains on the request path.");
  await wait(durations.process);

  setLane("feed", "Distributing the post inline", "active");
  logEvent("Feed distribution also blocks the response.");
  await wait(durations.fanout);

  showPost(caption, "Published");
  const publication = elapsedMilliseconds(startedAt);
  const result = { perceived: publication, acknowledgement: publication, publication };
  state.results.blocking = result;
  showCurrentMetrics(result);
  renderComparison();

  setLane("client", "Post finally visible", "success");
  setLane("api", "Response returned", "success");
  setLane("queue", "Unused", "idle");
  setLane("worker", "Complete", "success");
  setLane("feed", "One post visible", "success");
  logEvent("The blocking design reaches all three measured milestones together.");

  state.running = false;
  setControlsDisabled(false);
}

async function publishOnce(job) {
  if (state.publishedKeys.has(job.requestKey)) {
    state.duplicatesPrevented += 1;
    $("duplicates-metric").textContent = String(state.duplicatesPrevented);
    setLane("feed", "Duplicate publish prevented", "success");
    logEvent(`Duplicate publish prevented for ${job.requestKey}.`);
    return false;
  }

  state.publishedKeys.add(job.requestKey);
  setLane("feed", "Distributing accepted post", "active");
  await wait(durations.fanout);
  showPost(job.caption, "Published");
  job.publicationLatency = elapsedMilliseconds(job.startedAt);
  $("publication-metric").textContent = formatMilliseconds(job.publicationLatency);
  setLane("feed", "One post visible", "success");
  logEvent(`Published exactly one feed item for ${job.requestKey}.`);
  return true;
}

async function processJob(job, shouldLoseAcknowledgement) {
  setLane("queue", "Worker claimed one job", "active");
  setLane("worker", "Attempt 1: processing media", "active");
  logEvent(`Worker starts attempt 1 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);

  if (!shouldLoseAcknowledgement) {
    setLane("queue", "Job acknowledged", "success");
    setLane("worker", "Complete", "success");
    return;
  }

  setLane("worker", "Acknowledgement lost after publish", "danger");
  setLane("queue", "Same job scheduled for retry", "active");
  logEvent("Acknowledgement lost after publish. The queue will retry the same request key.");
  await wait(durations.retryDelay);

  setLane("worker", "Attempt 2: same request key", "active");
  logEvent(`Worker starts attempt 2 for ${job.requestKey}.`);
  await wait(durations.process);
  await publishOnce(job);
  setLane("queue", "Retry acknowledged", "success");
  setLane("worker", "Complete after safe retry", "success");
}

async function runAsync() {
  if (state.running) return;
  resetCurrentRun();
  state.running = true;
  setControlsDisabled(true);

  const startedAt = performance.now();
  const postId = ++state.postSequence;
  const job = {
    requestKey: `post-request-${postId}`,
    caption: $("caption").value || `Post ${postId}`,
    startedAt,
    publicationLatency: null
  };
  const shouldLoseAcknowledgement = $("fail-first").checked;

  showPost(job.caption, "Pending");
  const perceived = elapsedMilliseconds(startedAt);
  $("perceived-metric").textContent = formatMilliseconds(perceived);
  setLane("client", "Pending post visible immediately", "success");
  setLane("api", "Accepting request", "active");
  logEvent(`Client renders a pending card for ${job.requestKey}.`);

  await wait(durations.accept);
  const acknowledgement = elapsedMilliseconds(startedAt);
  $("ack-metric").textContent = formatMilliseconds(acknowledgement);
  setLane("api", "Accepted and queued", "success");
  setLane("queue", "One job waiting", "active");
  $("post-status").textContent = "Accepted";
  logEvent("API acknowledges accepted work before media processing finishes.");

  await processJob(job, shouldLoseAcknowledgement);
  const result = { perceived, acknowledgement, publication: job.publicationLatency };
  state.results.responsive = result;
  showCurrentMetrics(result);
  renderComparison();
  setLane("client", "Published state reconciled", "success");
  logEvent("Client preserves the first-publication measurement after any safe retry.");

  state.running = false;
  setControlsDisabled(false);
}

$("run-blocking").addEventListener("click", runBlocking);
$("run-async").addEventListener("click", runAsync);
$("reset-simulator").addEventListener("click", resetSimulator);

resetSimulator();

How to use this reference

Compare function boundaries first. Pay close attention to the duplicate guard inside publishOnce().

Check that runAsync() passes shouldLoseAcknowledgement into processJob().

Prove the retry stays idempotent

The retry path takes about three seconds because the simulator waits through two worker attempts. A short pause is expected.

Before you test it, do you expect attempt 2 to create a second card or stop at the publication boundary?

  • Return to Safari from earlier.
  • Select Fail first worker acknowledgement.
  • Click Run async flow.

You'll see Acknowledgement lost after publish followed by a second worker attempt using the same request key. The log then shows Duplicate publish prevented.

The Duplicates prevented metric reads 1. The simulated feed still contains one post.

The Responsive publication value remains the first successful publication measurement. Rejecting the duplicate does not overwrite it.

Is the retry producing two publications?

Confirm that attempt 2 receives the same job object. Creating another job would produce a different request key.

Check that state.publishedKeys.add(job.requestKey); runs before the first fan-out wait. The completed key must exist before redelivery reaches publishOnce().

Help me debug duplicate publication during the retry.

That is the correctness boundary working. Your simulator now survives redelivery without creating a second feed item.

Secret mission

Model the Celebrity Fan-Out Trade-Off

A post can publish quickly while distribution still creates substantial work behind the scenes. Extend the simulator with a toy model that compares immediate write work with deferred feed-read work as the audience grows.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your local simulator available, pause your work, or delete the project entirely. Keeping these files creates no ongoing service charges.

Resources you used:

  • Local posting-speed-simulator folder containing index.html, style.css, and app.js.

Keep everything running

No action needed. Choose this if you are still comparing posting flows or experimenting with the fan-out model.

  • Leave the posting-speed-simulator folder unchanged.
  • Switch back to the existing Safari tab whenever you want to run another simulation.
  • Keep Visual Studio Code open if you plan to inspect or modify the simulator code.

Pause - I'll come back to this later

Close the simulator surfaces to free up memory while preserving every project file for later.

  • Close the Safari tab that displays the local simulator.
  • Close the Visual Studio Code window that contains posting-speed-simulator.
  • Keep the posting-speed-simulator folder in its current location.

Your blocking flow, responsive flow, retry demonstration, measurements, and fan-out lab remain available inside the saved files.

Delete - I don't want to use this again

Remove the local simulator if you have finished experimenting with it. This project created no cloud resources or external data that require separate deletion.

Permanent deletion can feel drastic. The scope is limited to the local posting-speed-simulator folder.

  • Close the Safari tab that displays the simulator.
  • Close the Visual Studio Code window that contains the project.
  • Press Cmd+Space to open the macOS search bar.
  • Type Finder and press Enter to open it.
  • Use the search field in the top-right corner to find posting-speed-simulator.
  • Drag the posting-speed-simulator folder to Trash.

Emptying Trash permanently removes every item currently stored there. Check for unrelated files that you still need before you continue.

  • Review the other items currently stored in Trash.
  • Empty Trash after confirming that its contents can be deleted.
  • Search for posting-speed-simulator again to verify that the folder no longer appears.

The missing folder confirms that the simulator files have been removed from your Mac.

Nice Work!

Nice Work!

Excellent work! You built a browser-based simulator that makes posting latency visible without presenting its generic model as Instagram's private architecture.

What you learned:

  • Contrasted a blocking critical path with an asynchronous posting flow. Tracked the pending-to-published transition across all five system lanes.
  • Captured perceived latency with a monotonic browser clock. Measured server acknowledgement latency separately. Recorded publication completion time at the first successful publish. Preserved both designs in one comparison table.
  • Reproduced a lost worker acknowledgement after publication. Reused a stable request key during the retry. Enforced idempotency so the feed kept exactly one post.
  • Secret Mission: Built a toy fan-out trade-off lab. It recalculates fan-out-on-write from follower count. It recalculates fan-out-on-read from active-reader count. The outputs reveal how workload shape shifts work across write capacity versus read capacity.

Ready to quiz yourself?