<?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: GeekyAnts India Pvt Ltd</title>
    <description>The latest articles on DEV Community by GeekyAnts India Pvt Ltd (@geekyants-inc).</description>
    <link>https://dev.to/geekyants-inc</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%2F3805440%2Fb1d94ce3-8033-4b59-b443-beb091ccb36b.jpg</url>
      <title>DEV Community: GeekyAnts India Pvt Ltd</title>
      <link>https://dev.to/geekyants-inc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/geekyants-inc"/>
    <language>en</language>
    <item>
      <title>Taming Elasticsearch at Scale</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:03:11 +0000</pubDate>
      <link>https://dev.to/geekyants-inc/taming-elasticsearch-at-scale-5c0d</link>
      <guid>https://dev.to/geekyants-inc/taming-elasticsearch-at-scale-5c0d</guid>
      <description>&lt;p&gt;Optimize Elasticsearch for millions of records with better pagination, leaner payloads, efficient filtering, faster ingestion, and frontend techniques that keep search responsive with increase in data.&lt;/p&gt;

&lt;p&gt;Elasticsearch is useful for searching an increasing dataset that starts adding pressure on the database and the application. Instead of checking millions of records one by one, it builds an index that lets the system locate relevant data without scanning the entire dataset for every request. This makes it useful for full-text search, filtering, logs, product catalogs, reports, and other applications where users expect results as they type or filter.&lt;/p&gt;

&lt;p&gt;But adding Elasticsearch does not automatically make a search system fast. Query design, index mappings, response size, data ingestion, and &lt;a href="https://geekyants.com/engineering/web-engineering-services" rel="noopener noreferrer"&gt;frontend rendering&lt;/a&gt; can all affect how quickly a search feels to the person using it.&lt;/p&gt;

&lt;p&gt;Here is a full-stack guide to searching millions of records without freezing the frontend.&lt;/p&gt;

&lt;p&gt;It's a story most of us have lived through in some form.&lt;/p&gt;

&lt;p&gt;Your database starts groaning under the weight of a few million records. Every &lt;code&gt;LIKE %query%&lt;/code&gt; clause is a small act of violence against your CPU. So the team makes the obvious call: bring in Elasticsearch.&lt;/p&gt;

&lt;p&gt;For a while, it's magic. Queries come back in 15 milliseconds. Full-text search just works. Everyone's happy.&lt;/p&gt;

&lt;p&gt;Then, six months later, the index has grown into something with millions of dense, deeply nested documents, and the complaints start rolling in: "Every time I search the reports page, the screen freezes for four seconds, my fan spins up, and sometimes the tab just dies."&lt;/p&gt;

&lt;p&gt;Which is confusing, because Elasticsearch is built to query petabytes in milliseconds. So why does it feel like the frontend is choking?&lt;/p&gt;

&lt;p&gt;In practice, it almost never has anything to do with Elasticsearch's raw power. The real bottleneck is nearly always some combination of how the queries are written, how the data was indexed in the first place, how much JSON gets shipped over the wire, and what the browser is asked to do with it once it arrives.&lt;/p&gt;

&lt;p&gt;This is a walk through the places that tend to go wrong — on the backend, at index time, and in the browser — and what to do instead.&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%2Fpv0gch53a4pfbfa6gjmw.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%2Fpv0gch53a4pfbfa6gjmw.png" alt="Six Elasticsearch performance bottlenecks across pagination, payloads, filtering, indexing, ingestion, and frontend rendering" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Six independent bottlenecks — backend, indexing, and frontend — each with a small, specific fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The deep pagination trap
&lt;/h2&gt;

&lt;p&gt;Say you're building a paginated table. The natural instinct is page-number pagination:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;page&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;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&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;"match_all"&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="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;Looks harmless enough. Here's what it actually does to your cluster.&lt;/p&gt;

&lt;p&gt;If your index is spread across 5 shards and you ask for items 5,000–5,050, each shard has to hand its top 5,050 documents to the coordinator node — not just the 50 you want. That's 5 × 5,050 = 25,250 documents pulled into memory, sorted, and then almost entirely thrown away, just so the coordinator can hand you the 50 that were left over.&lt;/p&gt;

&lt;p&gt;By the time users are clicking into page 200, your cluster is burning most of its CPU sorting documents it's about to discard. Push it far enough and Elasticsearch will simply refuse — &lt;code&gt;from + size&lt;/code&gt; beyond 10,000 hits the &lt;code&gt;index.max_result_window&lt;/code&gt; limit by default. (Plenty of teams just raise that limit instead of fixing the underlying issue, which only postpones the memory problem.)&lt;/p&gt;

&lt;p&gt;There isn't one correct fix here — it depends on how people actually navigate your UI.&lt;/p&gt;

&lt;p&gt;If you have clickable page numbers ("1, 2, 3 ... 25") and people genuinely jump around, &lt;code&gt;from / size&lt;/code&gt; is fine. Just cap how deep it goes. Restricting navigation to roughly the first 200 pages (&lt;code&gt;from + size &amp;lt; 10,000&lt;/code&gt;) is safe for almost any real-world dataset.&lt;/p&gt;

&lt;p&gt;If you have infinite scroll or a "Load more" button, reach for &lt;code&gt;search_after&lt;/code&gt; instead:&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;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&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;"match_all"&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;"sort"&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;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"desc"&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;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asc"&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;"search_after"&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="s2"&gt;"2026-09-20T14:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"doc-10482"&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;Instead of asking for "page 50," you're saying "give me whatever comes right after this document." Each shard jumps straight to that point in its sorted index — no sorting through thousands of discarded documents, no memory spike. Item 50 and item 5,000,000 cost roughly the same.&lt;/p&gt;

&lt;p&gt;The catch: &lt;code&gt;search_after&lt;/code&gt; only moves forward. There's no jumping to an arbitrary page — it's a cursor, not a page number, and your UI needs to reflect that.&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%2Fj823qkq8xx9pt3kxpudx.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%2Fj823qkq8xx9pt3kxpudx.png" alt="Elasticsearch deep pagination with from and size compared with efficient search_after cursor pagination" width="800" height="783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;from/size&lt;/code&gt; forces every shard to hand over documents it then throws away. &lt;code&gt;search_after&lt;/code&gt; skips straight to where it left off.&lt;/p&gt;

&lt;p&gt;If you're exporting thousands of rows to CSV or PDF, use a Point-in-Time (PIT) reader combined with &lt;code&gt;search_after&lt;/code&gt;. PIT freezes a consistent snapshot of the index, so the export doesn't shift under you if documents get added mid-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Stop sending the whole warehouse back
&lt;/h2&gt;

&lt;p&gt;Documents in a real system tend to accumulate weight. Customer profiles, nested inspection logs, historical audit trails — it's not unusual for a single document to sit at 50–200 KB.&lt;/p&gt;

&lt;p&gt;Now return 50 of those without restricting fields, and you're shipping something like 5 MB of JSON for a single page load. And what actually happens with that 5 MB?&lt;/p&gt;

&lt;p&gt;It takes over a second just to download.&lt;/p&gt;

&lt;p&gt;The browser has to parse a multi-megabyte string, which locks up the main thread while it does.&lt;/p&gt;

&lt;p&gt;The UI stutters, animations skip, and the user assumes something's broken.&lt;/p&gt;

&lt;p&gt;Meanwhile, your table is probably showing five columns: ID, code, date, inspector, status.&lt;/p&gt;

&lt;p&gt;The fix is boring but effective — tell Elasticsearch exactly which fields you actually need:&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;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_source"&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="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"report_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"start_date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"inspector_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"status"&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;"query"&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="err"&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;Strip out the nested arrays and raw submission blobs you're not displaying, and that 5 MB response often drops to something like 40 KB. At that size, parsing takes a couple of milliseconds and the UI never even notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Don't score things that can't be scored
&lt;/h2&gt;

&lt;p&gt;Elasticsearch draws a real distinction between two kinds of query clauses:&lt;/p&gt;

&lt;p&gt;Query context (&lt;code&gt;must&lt;/code&gt;, &lt;code&gt;should&lt;/code&gt;) — "how well does this match?" It runs BM25 scoring and produces a &lt;code&gt;_score&lt;/code&gt;. Filter context (&lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;must_not&lt;/code&gt;) — "does this match, yes or no?" No scoring. Purely binary.&lt;/p&gt;

&lt;p&gt;A mistake that shows up constantly:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Slow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;forces&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scoring&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;things&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;that&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;don't&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;have&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"degree"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;matching&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;"query"&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;"bool"&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;"must"&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;"match"&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;"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;"approved"&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;span class="nl"&gt;"range"&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;"start_date"&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;"gte"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-01"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"match"&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;"project_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hospital"&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;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;Is one "approved" report more approved than another? Obviously not — it's binary. But by putting exact-match conditions like status and date inside &lt;code&gt;must&lt;/code&gt;, you're asking Elasticsearch to spend CPU scoring things that were never going to have a meaningful score. Worse, clauses inside &lt;code&gt;must&lt;/code&gt; skip the filter cache entirely.&lt;/p&gt;

&lt;p&gt;Move anything binary into &lt;code&gt;filter&lt;/code&gt;:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Fast&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;full-text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;part&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scored&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;"query"&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;"bool"&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;"must"&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;"match"&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;"project_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hospital"&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;span class="nl"&gt;"filter"&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;"term"&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;"status.keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approved"&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;span class="nl"&gt;"range"&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;"start_date"&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;"gte"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-01"&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;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="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;The payoff isn't just skipping scoring — Elasticsearch also caches filter clauses as bitmap structures in memory. The next time anyone filters by &lt;code&gt;status: approved&lt;/code&gt;, that part of the query resolves in microseconds without touching the disk.&lt;/p&gt;

&lt;p&gt;A decent rule of thumb: if a field is either "yes" or "no," with no concept of how well it matches, it belongs in the &lt;code&gt;filter&lt;/code&gt;.&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%2Fuuea2mk67w6jb9k0jgpk.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%2Fuuea2mk67w6jb9k0jgpk.png" alt="Elasticsearch query context with scored matches compared with cached filter context for exact conditions" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same conditions, two different execution paths. Binary conditions belong on the right.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The leading wildcard problem
&lt;/h2&gt;

&lt;p&gt;When people want search-as-you-type or partial matching, the tempting shortcut looks like this:&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;"query"&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;"query_string"&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*concrete*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&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="s2"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sample_description"&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="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;A leading wildcard — &lt;code&gt;*concrete&lt;/code&gt;, not &lt;code&gt;concrete*&lt;/code&gt; — essentially disables the inverted index. Think of the index like the back of a textbook: search for "concrete" and you flip straight to the right page. Search for &lt;code&gt;*concrete*&lt;/code&gt; and the book is useless to you — you're back to reading every word on every page, because there's no way to know in advance what might precede "concrete."&lt;/p&gt;

&lt;p&gt;Run that across a few million documents and you get exactly the CPU spikes and multi-second delays you'd expect.&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%2Fk4om7m1nyqxax9pqb0rg.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%2Fk4om7m1nyqxax9pqb0rg.png" alt="A leading wildcard defeats the inverted index. A prefix built at index time turns the same lookup into one hop." width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A leading wildcard defeats the inverted index. A prefix built at index time turns the same lookup into one hop.&lt;/p&gt;

&lt;p&gt;The real fix happens at index time, not query time. The simplest route is Elasticsearch's built-in &lt;code&gt;search_as_you_type&lt;/code&gt; field type. First, declare the field this way when you create (or update) your mapping:&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;PUT /reports
{
  "mappings": {
    "properties": {
      "sample_description": {
        "type": "search_as_you_type"
      }
    }
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Elasticsearch quietly builds sub-fields behind the scenes (&lt;code&gt;sample_description._2gram&lt;/code&gt;, &lt;code&gt;._3gram&lt;/code&gt;, and so on) that store prefix tokens. The word "concrete" ends up indexed as something like &lt;code&gt;["co", "con", "conc", "concr", "concre", "concret", "concrete"]&lt;/code&gt;. Then query with &lt;code&gt;bool_prefix&lt;/code&gt; instead of a wildcard:&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;"query"&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;"multi_match"&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"conc"&lt;/span&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;"bool_prefix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&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="s2"&gt;"sample_description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sample_description._2gram"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sample_description._3gram"&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;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;Now typing "conc" resolves as a direct term lookup against pre-built tokens instead of a full scan of the cluster.&lt;/p&gt;

&lt;p&gt;The tradeoff: n-gram indexing inflates the size of the affected fields, typically by something like 2–3x, since you're now storing every prefix variant on disk. For most applications that's a non-issue next to the speed gain — but if storage is tight, you can bound the range (&lt;code&gt;min_gram: 3&lt;/code&gt;, &lt;code&gt;max_gram: 10&lt;/code&gt;) to keep it in check.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The part everyone forgets: how you write data in
&lt;/h2&gt;

&lt;p&gt;Most of the attention in Elasticsearch tuning goes to queries, but a surprising share of real-world slowness starts at ingestion. This pattern shows up in almost every codebase the first time a team wires up Elasticsearch:&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="c1"&gt;# One HTTP request per document — the classic mistake
&lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_all_pending_reports&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;report&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elasticsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5,000 reports means 5,000 separate HTTP round-trips, each one negotiating its own connection and potentially triggering a refresh. What should take a few seconds can easily stretch into 10–15 minutes.&lt;/p&gt;

&lt;p&gt;The Bulk API exists precisely for this — batch hundreds or thousands of operations into a single request:&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;reports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_all_pending_reports&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&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;report&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_index&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;reports&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;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# 500 docs per flush (action + doc = 2 lines each)
&lt;/span&gt;        &lt;span class="n"&gt;elasticsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;elasticsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# flush whatever's left
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batching 500 documents per call turns 5,000 individual requests into 10. The difference is dramatic in practice — bulk indexing typically runs somewhere around 10–50x faster than indexing one document at a time.&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%2F9utg61b8li4ftv179t4h.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%2F9utg61b8li4ftv179t4h.png" alt="Individual Elasticsearch indexing requests compared with Bulk API batching for 5,000 documents" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same 5,000 documents, radically fewer round trips&lt;/p&gt;

&lt;p&gt;One request per document&lt;/p&gt;

&lt;p&gt;Two smaller things worth doing alongside this:&lt;/p&gt;

&lt;p&gt;Loosen the refresh interval during heavy writes. By default, Elasticsearch refreshes every index every second so new documents become searchable almost immediately. During a large backfill, that constant refreshing just burns CPU for no benefit. Turn it down while you're bulk-loading:&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;PUT /reports/_settings
{ "index": { "refresh_interval": "30s" } }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and set it back once you're done:&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;PUT /reports/_settings
{ "index": { "refresh_interval": "1s" } }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give documents a stable ID. If a pipeline ever retries after a partial failure, it can easily index the same record twice. Use a consistent ID — your database primary key works well — so Elasticsearch overwrites the old copy instead of quietly duplicating it:&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="c1"&gt;# Passing "id" turns this into an upsert — no duplicate rows
&lt;/span&gt;&lt;span class="n"&gt;elasticsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Don't let the browser undo all of that work
&lt;/h2&gt;

&lt;p&gt;Even a 20-millisecond Elasticsearch response can still end in a frozen tab if the frontend isn't careful. Three things worth having in place:&lt;/p&gt;

&lt;p&gt;Debounce input. Typing "Bridge Inspection" is 17 keystrokes in about a second and a half. Fire a request per keystroke and you've just sent 17 overlapping searches. A 300ms debounce — wait until the user actually pauses — fixes this outright.&lt;/p&gt;

&lt;p&gt;Cancel stale requests. If someone types "B" and then quickly finishes "Bridge," the request for "B" is now irrelevant. If it happens to resolve after the "Bridge" request, your table briefly shows the wrong results. &lt;code&gt;AbortController&lt;/code&gt; handles this cleanly:&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;let&lt;/span&gt; &lt;span class="nx"&gt;currentController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchSearchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;currentController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// kill the previous request&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;currentController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;renderTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fetch error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&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;Batch your DOM writes. Inserting 200 rows one at a time into a live table forces the browser to recalculate layout after each insertion. Build everything off-screen first, then insert it in one shot:&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;function&lt;/span&gt; &lt;span class="nf"&gt;renderTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tableBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tableBody&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fragment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocumentFragment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
      &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;report_code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inspector_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;tableBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;tableBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// single repaint&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth flagging: building rows this way with &lt;code&gt;innerHTML&lt;/code&gt; is only safe if none of those fields can contain arbitrary user text. If &lt;code&gt;inspector_name&lt;/code&gt; or similar could ever hold something a user typed, that's an XSS opening — building rows with &lt;code&gt;textContent&lt;/code&gt; instead closes it off.&lt;/p&gt;

&lt;p&gt;Past 500 rows or so, consider a virtualized table that only renders the 15–20 rows actually visible in the viewport, rather than materializing the entire result set as DOM nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like put together
&lt;/h2&gt;

&lt;p&gt;None of these changes are individually dramatic. Together, they compound. For an application with roughly 800,000 indexed documents, applying all six optimizations could produce improvements in this range:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search response time&lt;/td&gt;
&lt;td&gt;~3.2s&lt;/td&gt;
&lt;td&gt;~45ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON payload size&lt;/td&gt;
&lt;td&gt;~4.8 MB&lt;/td&gt;
&lt;td&gt;~38 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend render time&lt;/td&gt;
&lt;td&gt;~1,400ms&lt;/td&gt;
&lt;td&gt;~12ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk reindex (5,000 docs)&lt;/td&gt;
&lt;td&gt;~14 min&lt;/td&gt;
&lt;td&gt;~8 sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster CPU during search&lt;/td&gt;
&lt;td&gt;~78% sustained&lt;/td&gt;
&lt;td&gt;Single-digit spikes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(These are illustrative figures based on the kind of improvement this set of changes typically produces — worth replacing with your own measured numbers once you've made the changes on your own system.)&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%2Fpt7h5c4b5cdkkbe4e5cs.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%2Fpt7h5c4b5cdkkbe4e5cs.png" alt="Elasticsearch optimization results for response time, payload size, rendering, reindexing, and CPU usage" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every bar comes from the before/after table above — same numbers, easier to compare at a glance.&lt;/p&gt;

&lt;p&gt;A 45ms response with a 38KB payload that renders in 12ms doesn't just feel fast — it feels instant, even on a phone over 4G.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheatsheet
&lt;/h2&gt;

&lt;p&gt;Use this cheatsheet to quickly match common Elasticsearch performance issues with their likely cause and fix.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;What's Actually Happening&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deep pagination (&lt;code&gt;from: 5000&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Coordinator sorts and discards thousands of docs per shard&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;search_after&lt;/code&gt; for infinite scroll; cap &lt;code&gt;from/size&lt;/code&gt; for page-number UIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Giant payloads (5MB JSON)&lt;/td&gt;
&lt;td&gt;Full documents returned when only a few fields are shown&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_source&lt;/code&gt; filtering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow filtering&lt;/td&gt;
&lt;td&gt;Exact-match fields scored unnecessarily&lt;/td&gt;
&lt;td&gt;Move to &lt;code&gt;bool.filter&lt;/code&gt; for caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leading wildcards (&lt;code&gt;*query*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Inverted index can't be used, full scan instead&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;search_as_you_type&lt;/code&gt; + n-grams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow bulk imports&lt;/td&gt;
&lt;td&gt;One HTTP request per document&lt;/td&gt;
&lt;td&gt;Bulk API, batched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI freezes while typing&lt;/td&gt;
&lt;td&gt;No debounce, requests racing&lt;/td&gt;
&lt;td&gt;300ms debounce + &lt;code&gt;AbortController&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table rendering stutters&lt;/td&gt;
&lt;td&gt;DOM reflow on every row insert&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DocumentFragment&lt;/code&gt; or virtualization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Underlying Idea
&lt;/h2&gt;

&lt;p&gt;Every fix above is really the same move, applied in a different place: figure out where work is happening, and ask whether it's happening at the right time.&lt;/p&gt;

&lt;p&gt;Push computation to index time (n-grams, explicit mappings) instead of query time (wildcards). Push filtering into the cache layer (&lt;code&gt;bool.filter&lt;/code&gt;) instead of the scoring layer (&lt;code&gt;bool.must&lt;/code&gt;). Push payload decisions onto the backend (&lt;code&gt;_source&lt;/code&gt; filtering) instead of asking the browser to parse everything and use a fraction of it. Push DOM construction into memory (&lt;code&gt;DocumentFragment&lt;/code&gt;) instead of doing it row-by-row on the live page.&lt;/p&gt;

&lt;p&gt;If you want a concrete place to start: open your slowest search endpoint right now and add &lt;code&gt;_source&lt;/code&gt; filtering so it only returns the fields your UI actually shows. Check the response size before and after. That alone is usually a five-minute change that cuts payload by 90% and makes the page feel noticeably different.&lt;/p&gt;

&lt;p&gt;Everything else in this piece can follow from there, one endpoint at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn Elasticsearch Performance Into Product Performance
&lt;/h2&gt;

&lt;p&gt;A fast Elasticsearch query means little if the API sends megabytes of JSON or the browser spends a second rendering it. Search performance has to be addressed across indexing, APIs, infrastructure, and the frontend. GeekyAnts helps teams optimize these systems through &lt;a href="https://geekyants.com/engineering/backend/enterprise-backend-development" rel="noopener noreferrer"&gt;enterprise backend development and modernization&lt;/a&gt;, covering backend architecture, &lt;a href="https://geekyants.com/engineering/backend/data-engineering-services" rel="noopener noreferrer"&gt;data pipelines&lt;/a&gt;, &lt;a href="https://geekyants.com/engineering/quality-assurance/performance-engineering" rel="noopener noreferrer"&gt;performance tuning&lt;/a&gt;, and systems built to handle growing data volumes.&lt;/p&gt;

&lt;p&gt;The goal is simple: keep search responsive as the dataset grows, without making every new record or user request more expensive than it needs to be.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Building a Real-Time Photo Sync Video Experience Using 100ms SDK and Reactive State Management</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:08:14 +0000</pubDate>
      <link>https://dev.to/geekyants/building-a-real-time-photo-sync-video-experience-using-100ms-sdk-and-reactive-state-management-4lf4</link>
      <guid>https://dev.to/geekyants/building-a-real-time-photo-sync-video-experience-using-100ms-sdk-and-reactive-state-management-4lf4</guid>
      <description>&lt;p&gt;This blog explains how to build a real-time video experience with synchronized photo sharing, chat, and reactive state management using the 100ms SDK.&lt;/p&gt;

&lt;p&gt;Building &lt;a href="https://geekyants.com/blog/designing-a-real-time-ai-pipeline-for-human-like-video-conversations" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;real-time video experiences&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt; traditionally required deep expertise in WebRTC, signaling infrastructure, media synchronization, and browser-specific handling. With modern SDKs like 100ms, teams can now focus more on product experience and less on low-level media infrastructure.&lt;/p&gt;

&lt;p&gt;In this article, we will walk through how to build a real-time video calling experience with synchronized photo sharing and chat, powered by the 100ms JavaScript SDK, a reactive state architecture, and a &lt;a href="https://geekyants.com/engineering/web-engineering-services" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;modular frontend design&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core concepts of 100ms architecture&lt;/li&gt;
&lt;li&gt;Pre-join (green room) experience&lt;/li&gt;
&lt;li&gt;Joining and managing a live video room&lt;/li&gt;
&lt;li&gt;Real-time photo synchronization across participants&lt;/li&gt;
&lt;li&gt;Chat integration using broadcast messaging&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://geekyants.com/blog/react-query-as-a-state-manager-in-nextjs-do-you-still-need-redux-or-zustand" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;State management&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt; strategies for responsive UI&lt;/li&gt;
&lt;li&gt;Synchronization correctness, conflict resolution, and reconnect handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;High-Level Architecture&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Before diving into the implementation, it’s worth understanding how the system components interact.&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%2Fgi56c5i1froob7skbrcp.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%2Fgi56c5i1froob7skbrcp.png" alt=" " width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the backend, when a meetup is created, the server calls the 100ms Rooms API to provision a room and generate authentication tokens for invited participants. Each participant receives a token tied to the same room ID, ensuring everyone joins the same session.&lt;/p&gt;

&lt;p&gt;On the frontend, a single HMSReactiveStore instance acts as the centralized source of truth for media state, participants, tracks, and messages. Both the Green Room and the Live Call screens subscribe to slices of this store rather than maintaining disconnected local state.\&lt;br&gt;
The SDK exposes three key primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; → Reactive room state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions&lt;/strong&gt; → Methods for operations such as join, mute, or messaging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; → Event listeners for peer updates and room events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture provides a predictable data flow for real-time applications and keeps media state, participant state, and UI subscriptions centralized. The synchronized-photo feature still requires explicit conflict-resolution and lifecycle handling, which we address below.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Why a Pre-Join (Green Room) Matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A pre-join experience is critical for user confidence and system readiness.\&lt;br&gt;
100ms provides a &lt;strong&gt;preview API&lt;/strong&gt; that allows users to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test camera and microphone&lt;/li&gt;
&lt;li&gt;Set initial mute/unmute states&lt;/li&gt;
&lt;li&gt;Validate network readiness and authentication before joining&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Key Implementation Flow&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fetch authentication token from backend&lt;/li&gt;
&lt;li&gt;Call preview() with user config&lt;/li&gt;
&lt;li&gt;Attach local video track to UI&lt;/li&gt;
&lt;li&gt;Allow user to toggle mic and camera
&lt;/li&gt;
&lt;/ol&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="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;token from backend&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&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;Once preview is successful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local tracks are created&lt;/li&gt;
&lt;li&gt;Permissions are verified&lt;/li&gt;
&lt;li&gt;Settings persist into the join phase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces friction during room entry.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Setting Up the 100ms SDK&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The 100ms JavaScript SDK ships through @100mslive/hms-video-store.\&lt;br&gt;
Everything flows from a single HMSReactiveStore instance:&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="c1"&gt;// Initialize once — treated as a module-level singleton&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HMSReactiveStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@100mslive/hms-video-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmsManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HMSReactiveStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;triggerOnSubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// fire callbacks immediately with current state&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmsNotifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling triggerOnSubscribe() is important because it ensures subscribers immediately receive the current SDK state instead of waiting for the next state update.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Reading State with Selectors&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The store exposes state through memoized selectors rather than raw state objects. This enables efficient reactive subscriptions.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;selectPeers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectIsLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectIsLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectLocalVideoTrackID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectLocalPeer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectTrackByID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="nx"&gt;selectPeerByID&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@100mslive/hms-video-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// One-time read&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isVideoOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectIsLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Reactive subscription — returns an unsubscribe function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;enabled&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;// runs whenever local video enabled/disabled&lt;/span&gt;
  &lt;span class="nf"&gt;updateVideoButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectIsLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subscribing to fine-grained selectors helps avoid unnecessary re-renders. In our implementation, this was useful for participant and track updates because changes could be scoped to the affected selector rather than causing broad UI updates.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Green Room: Pre-Join Experience&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The Green Room acts as a staging area before users join the room.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Attaching Local Video&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once preview mode starts, we subscribe to the local video track and attach it to a &amp;lt;video&amp;gt; element.&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="c1"&gt;// Wire local video into a &amp;lt;video&amp;gt; element&lt;/span&gt;
&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&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;trackId&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectLocalVideoTrackID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user disables their camera, the video track disappears and the UI reflects the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Toggling Camera and Microphone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Media controls during preview work the same way as they do during a live call.&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="c1"&gt;// Toggle camera&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectIsLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Toggle mic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectIsLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the UI subscribes to the store, button states remain synchronized.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Transitioning to the Live Call&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When the user clicks "Join", we stop the preview session first by calling hmsActions.leave(). This releases the preview peer from the 100ms session and prevents a ghost local peer from remaining alongside the new peer after the user joins. After leave completes, we navigate to the Live Call route.&lt;/p&gt;

&lt;p&gt;The camera/microphone preferences chosen in the Green Room are persisted to a Pinia store (with localStorage persistence) so the Live Call can restore them on mount without asking the user again.\&lt;br&gt;
To join a room, 100ms requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Auth token&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Username&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The join flow is straightforward:&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="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;savedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Restore preferences from Green Room&lt;/span&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;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preferredCameraOn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preferredMicOn&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;After joining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SDK emits events such as ON_JOIN, ON_PEER_UPDATE, and ON_TRACK_UPDATE&lt;/li&gt;
&lt;li&gt;Reactive store subscriptions keep the UI aligned with the resulting SDK state&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Rendering the Peer Strip&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;All connected peers are available via the selectPeers selector. We subscribe to it and re-render the participant strip whenever anyone joins or leaves. The local peer is sorted to the front; remote peers are sorted by join time.&lt;/p&gt;

&lt;p&gt;Each peer tile has a &amp;lt;video&amp;gt; element whose DOM reference is stored in a Map keyed by peer ID. When a peer's video track changes (e.g. they turn their camera on), we call attachVideo to bind the new track. When a peer leaves, we call detachVideo to clean up.&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="c1"&gt;// Subscribe to the peer list — fires on every join/leave&lt;/span&gt;
&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;peers&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="nf"&gt;renderParticipants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectPeers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Attach video for a specific peer&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;attachPeerVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoTrack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoTrack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;);&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;This provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI updates when participants join or leave&lt;/li&gt;
&lt;li&gt;Consistent track state across components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The store acts as a single source of truth for SDK-backed room state, while application-specific state such as the current synchronized photo is handled separately with explicit synchronization rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Showing Live Audio and Video Status&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For remote peers, track enabled/disabled state is read from the store using selectTrackByID. For the local peer, we use selectIsLocalVideoEnabled and selectIsLocalAudioEnabled directly. This allows the mic icon next to each participant tile to update in real time as participants toggle their devices, without polling.&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="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle track changes dynamically&lt;/li&gt;
&lt;li&gt;Detach video when track is removed&lt;/li&gt;
&lt;li&gt;Retry attaching in case of async delays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Media toggling is handled via actions:&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="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalVideoEnabled&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State is synced using selectors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selectIsLocalVideoEnabled&lt;/li&gt;
&lt;li&gt;selectIsLocalAudioEnabled&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Persisting Preferences&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Camera and microphone preferences are stored locally and reused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;During preview&lt;/li&gt;
&lt;li&gt;During join&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a consistent user experience across sessions.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Real-Time Chat via Broadcast Messages&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Rather than spinning up a separate WebSocket or a third-party chat service, we used 100ms's built-in messaging primitives. hmsActions.sendBroadcastMessage(text) delivers a message to every peer in the room. The SDK stores all messages in HMSStore and exposes them via the selectHMSMessages selector.&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="c1"&gt;// Send a chat message&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBroadcastMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// React to new messages&lt;/span&gt;
&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;messages&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;// Filter to 'chat' type only — other types are used internally (e.g. photo sync)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chatMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;renderMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectHMSMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important design decision: messages are typed. The default type for a regular chat message is 'chat'. We use a custom type 'PHOTO_SYNC' for the carousel sync messages described in the next section. The chat panel filters to type === 'chat' so photo-sync events never appear as chat bubbles.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Unread Message Badge&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We track how many chat messages were visible the last time the panel was open (lastReadCount). When a new message arrives while the panel is closed and the sender isn't the local peer, we set an unread flag that drives a red dot on the chat button. When the panel opens, we reset the count.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Synced Image Carousel Across All Peers&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The synchronized carousel required a separate state-management approach. We wanted participants to converge on the same photo when someone navigated the carousel. We built the synchronization on top of 100ms's broadcast message API, with no additional signaling infrastructure.&lt;/p&gt;

&lt;p&gt;An important distinction is that these messages are navigation events, not durable shared state. Broadcast messaging works for transient actions such as ‘move to photo 5,’ but it should not serve as the canonical state for late joiners. If the application needs the current photo to survive missed messages or reconnects, the 100ms Session Store is a better fit for a canonical session-level state.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Defining the Sync Message Shape&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We define a typed interface for the payload so that both the sender and all receivers agree on the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PhotoSyncMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate_photo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;photoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary resolution: unique ID of the photo&lt;/span&gt;
  &lt;span class="nl"&gt;navigatorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// which peer triggered the change&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Diagnostic metadata (clocks may drift, do not use for ordering)&lt;/span&gt;
  &lt;span class="nl"&gt;sequenceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Monotonic counter to reject stale messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Note on Synchronization Correctness&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To prevent concurrent navigation conflicts (e.g., two users changing photos simultaneously) and distributed clock drift issues, this architecture enforces an authoritative navigator model. The presenter acts as the authoritative navigator. Navigation initiated by other participants is ignored by receivers, preventing simultaneous navigation from producing conflicting shared state. Each accepted navigation event also carries a monotonic sequence ID, allowing receivers to reject stale events.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Sending a Navigation Event&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Whenever the local user taps a thumbnail or presses a carousel arrow, we broadcast a PHOTO_SYNC message alongside updating local state:&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;AUTHORITATIVE_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presenter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Module-level sequence tracker &lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;localSequenceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePhotoSelected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only the authoritative navigator can initiate synced navigation &lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;AUTHORITATIVE_ROLE&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;localSequenceId&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCurrentPhotoId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// local update&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBroadcastMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate_photo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;photoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;navigatorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;sequenceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;localSequenceId&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PHOTO_SYNC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;         &lt;span class="c1"&gt;// custom type keeps it separate from chat&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canonical_photo_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to update canonical session state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&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;h2&gt;
  
  
  &lt;strong&gt;Receiving and Applying Navigation Events&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;On the receiver side, we hook into hmsNotifications.onNotification(), filter to NEW_MESSAGE events with our custom type, parse and validate the payload, and resolve the target photo by its stable ID before updating local state.&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="c1"&gt;// Track the latest applied sync sequence for the room&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastSyncSequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Define which role is allowed to navigate to prevent simultaneous conflicts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AUTHORITATIVE_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presenter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

&lt;span class="nx"&gt;hmsNotifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNotification&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;notification&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NEW_MESSAGE&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
  &lt;span class="k"&gt;catch&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate_photo&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// --- 1. AUTHORITATIVE NAVIGATOR CHECK ---&lt;/span&gt;

&lt;span class="c1"&gt;// Restrict synchronization to an authoritative peer role.&lt;/span&gt;
&lt;span class="c1"&gt;// This prevents multiple participants from independently&lt;/span&gt;
&lt;span class="c1"&gt;// updating the shared navigation state.&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;senderPeer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectPeerByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigatorId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;senderPeer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roleName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;AUTHORITATIVE_ROLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rejected navigation event from non-authoritative peer&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// --- 2. DETERMINISTIC SEQUENCE ORDERING ---&lt;/span&gt;
  &lt;span class="c1"&gt;// Reject out-of-order/stale messages from the authoritative peer&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequenceId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;lastSyncSequence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rejected stale navigation sequence&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// --- 3. STABLE ID RESOLUTION (No Index Fallback) ---&lt;/span&gt;
  &lt;span class="c1"&gt;// Match strictly by stable photoId. If lists diverge and ID is unknown, &lt;/span&gt;
  &lt;span class="c1"&gt;// do not guess using index. Ignore event and rely on Session Store recovery.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetPhoto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photoId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;targetPhoto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Photo ID not found in local state, ignoring event.&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Apply state and update tracker&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCurrentPhotoId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetPhoto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;lastSyncSequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequenceId&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;h2&gt;
  
  
  &lt;strong&gt;Syncing New Joiners&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A participant who joins mid-session could see the carousel start at index 0 while the rest of the room has moved to another photo. For late joiners, the implementation relies on the 100ms Session Store instead of allowing multiple peers to broadcast competing states. When a user joins, they simply read canonical_photo_id from the Session Store to land on the correct image.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Persisting Media Preferences Between Screens&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The user selects their camera and mic state in the Green Room. These preferences need to persist through the route change to the Live Call screen and a page refresh.&lt;/p&gt;

&lt;p&gt;We store preferredCameraOn and preferredMicOn in a Pinia store configured with selective localStorage persistence. Only these two fields are persisted; sensitive data like the HMS auth token is kept in memory only and is re-fetched on every session.&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="c1"&gt;// Pinia store — persist only media prefs&lt;/span&gt;
&lt;span class="nx"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-meetup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferredCameraOn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferredMicOn&lt;/span&gt;&lt;span class="dl"&gt;'&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;When the Live Call screen mount, we read these two values back and call setLocalVideoEnabled / setLocalAudioEnabled immediately after join() resolves. This means the peer publishes with the camera and microphone state configured in the Green Room.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Live Session Teardown&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Proper session teardown prevents stale participant and media state.&lt;/p&gt;

&lt;p&gt;If a participant disconnects without completing session teardown, other users may see stale or ‘ghost’ participants for a short period.&lt;/p&gt;

&lt;p&gt;We handle cleanup in three places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit leave button&lt;/li&gt;
&lt;li&gt;Browser unload events&lt;/li&gt;
&lt;li&gt;Component unmount lifecycle hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For browser lifecycle handling, do not rely on unload alone. Modern browsers may skip or limit unload processing, so lifecycle handling should account for pagehide, beforeunload, and freeze where appropriate. These events should be treated as best-effort lifecycle signals rather than a guarantee that an awaited asynchronous leave() operation will finish before the page disappears.&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;explicitTeardown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;unsubscribers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;?.());&lt;/span&gt;
  &lt;span class="c1"&gt;// For explicit leave button, we safely await&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// For browser lifecycle (closing tab, backgrounding, freezing)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setupBrowserLifecycleTeardown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bestEffortCleanup&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;unsubscribers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;?.());&lt;/span&gt;
    &lt;span class="c1"&gt;// Do not await leave() during lifecycle events; it's a best-effort fire-and-forget&lt;/span&gt;
    &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Modern alternatives to the deprecated 'unload' event&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pagehide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestEffortCleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeunload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestEffortCleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;freeze&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestEffortCleanup&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;For an explicit leave action or route transition, the async leave() call can be awaited normally. For browser termination events, cleanup should be best-effort and should not assume asynchronous work will always complete. The application should also remove subscriptions and detach UI references during component teardown, while relying on the SDK/browser lifecycle for connection termination.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Performance and UX Considerations&lt;/strong&gt;
&lt;/h1&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Avoid Duplicate State&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Use the reactive store instead of local component state wherever possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Lazy Rendering&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Attach video tracks only when elements are mounted.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Optimistic UI Updates&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Update the UI when the user changes a photo, then synchronize the change.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Graceful Error Handling&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Token failures&lt;/li&gt;
&lt;li&gt;Permission issues&lt;/li&gt;
&lt;li&gt;Network interruptions&lt;/li&gt;
&lt;li&gt;Reconnects and late joiners&lt;/li&gt;
&lt;li&gt;Unknown or stale PHOTO_SYNC messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For synchronized navigation, network interruption is especially important: a participant may miss transient broadcast messages. The design should recover from canonical session-level state when the product requires convergence after reconnect.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Synchronization Validation and Failure Cases&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Before describing a real-time synchronization flow as production-ready, it is important to validate failure modes rather than relying only on the happy path. Useful acceptance checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rapid navigation:&lt;/strong&gt; repeatedly move forward/backward and verify that older messages do not overwrite the latest accepted sequence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simultaneous navigation:&lt;/strong&gt; have two participants navigate at the same time and verify that the defined authority/conflict-resolution rule produces one deterministic final state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late join:&lt;/strong&gt; join after the room has already moved through several photos and verify that the participant converges to the canonical current photo rather than starting at index 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect:&lt;/strong&gt; interrupt a participant's network connection, restore it, and verify that the participant recovers the current session state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Divergent photo ordering:&lt;/strong&gt; use stable photoId matching rather than assuming every participant's array has identical ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser lifecycle:&lt;/strong&gt; leave through the explicit button, route away, background/freeze the page, and close the tab to verify that subscriptions and media references are cleaned up as far as the browser lifecycle permits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks make the synchronization guarantees explicit. When behavior has not been measured under a defined participant count or network condition, describe the implementation as a maintainable foundation rather than making an absolute scalability or production-readiness claim.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Extensibility Opportunities&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;This architecture can support additional capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen sharing&lt;/li&gt;
&lt;li&gt;Role-based permissions&lt;/li&gt;
&lt;li&gt;&lt;a href="https://geekyants.com/blog/building-production-grade-video-thumbnail-scrubbing-in-the-browser-hls-frame-extraction-caching-and-performance-trade-offs" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;Recording and streaming&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Collaborative annotations&lt;/li&gt;
&lt;li&gt;Session-level shared state using the Session Store&lt;/li&gt;
&lt;li&gt;Deterministic conflict resolution for collaborative controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Key Takeaways&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Building this feature reinforced several architectural patterns for real-time applications:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Centralized reactive state simplifies synchronization&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Maintaining a single HMSReactiveStore instance prevents UI desynchronization and keeps media state consistent across screens.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Preview mode improves pre-join UX&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The Green Room gives users confidence and control before entering the live session.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Broadcast messaging can support multiple event types&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We used the same messaging layer for chat and photo-navigation events by namespacing message types. For durable shared state, such as the current photo for late joiners, session-level state is a better model.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Fine-grained subscriptions improve performance&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Track-level subscriptions prevent unnecessary participant list re-renders.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Clean teardown logic matters&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Explicit transitions should await leave(), while browser lifecycle cleanup must be treated as best-effort and designed around modern lifecycle events rather than assuming unload will complete asynchronous work.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Real-time collaboration is no longer limited to basic video calls.&lt;/p&gt;

&lt;p&gt;Combining 100ms’s reactive SDK architecture with a structured frontend state model supports interactive shared experiences without requiring a separate synchronization layer for every feature.&lt;/p&gt;

&lt;p&gt;The synchronized-media feature also highlights an important engineering lesson: real-time UX requires more than broadcasting an event. Correctness depends on defining the source of truth, resolving concurrent updates, rejecting stale messages, handling late joiners and reconnects, and managing cleanup across browser lifecycle states.&lt;/p&gt;

&lt;p&gt;With those considerations in place, the patterns described here provide a maintainable foundation for collaborative experiences involving shared media, live messaging, and reactive participant management.\&lt;br&gt;
As real-time applications expand beyond traditional conferencing, reactive synchronization, centralized media state, and event-driven collaboration provide a foundation for shared digital experiences.&lt;/p&gt;

&lt;p&gt;For teams building real-time video and interactive media products, GeekyAnts provides &lt;a href="https://geekyants.com/industry-expertise/media-entertainment" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;Media &amp;amp; Entertainment software development services&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt; for developing and scaling digital media experiences.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From Test Failure to Root Cause: Building an AI-Assisted Playwright Failure Analysis Pipeline</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:05:32 +0000</pubDate>
      <link>https://dev.to/geekyants-inc/from-test-failure-to-root-cause-building-an-ai-assisted-playwright-failure-analysis-pipeline-3m9g</link>
      <guid>https://dev.to/geekyants-inc/from-test-failure-to-root-cause-building-an-ai-assisted-playwright-failure-analysis-pipeline-3m9g</guid>
      <description>&lt;p&gt;This blog explains how an AI-assisted Playwright pipeline can analyze test failure evidence, identify probable root causes, and support human-validated debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Modern applications are increasingly tested through automated &lt;a href="https://geekyants.com/engineering/devops/cicd-release-management-services" rel="noopener noreferrer"&gt;&lt;strong&gt;CI/CD pipelines&lt;/strong&gt;&lt;/a&gt;. Tools such as &lt;a href="https://geekyants.com/blog/automation-testing-with-playwright-using-javascript" rel="noopener noreferrer"&gt;&lt;strong&gt;Playwright&lt;/strong&gt;&lt;/a&gt; allow teams to execute hundreds or thousands of browser tests consistently across environments. However, automation solves only one part of the problem: detecting that something failed. &lt;/p&gt;

&lt;p&gt;The more time-consuming problem often starts after the failure.&lt;/p&gt;

&lt;p&gt;A CI pipeline may report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED checkout.spec.ts 

TimeoutError: 

Expected "Order confirmed" to be visible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this message does not immediately tell the engineer what actually went wrong. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Was the application broken? &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Was the API unavailable? &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Was the test data incorrect? &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Did a network request fail? &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Was the locator outdated? &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Was there a race condition? &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As automation suites grow, engineers can spend considerable time collecting screenshots, checking logs, opening traces, inspecting network requests, and reproducing failures locally. &lt;/p&gt;

&lt;p&gt;This creates an opportunity for AI. &lt;/p&gt;

&lt;p&gt;Instead of allowing AI to execute or modify tests autonomously, we can use it as a failure-analysis layer that consumes the evidence already produced by Playwright and generates an evidence-backed diagnosis for an engineer to validate.&lt;/p&gt;

&lt;p&gt;The resulting workflow is:&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%2Flx9xxknuybrgq2neonhw.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%2Flx9xxknuybrgq2neonhw.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal is not to replace the &lt;a href="https://geekyants.com/service/hire-quality-assurance-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;QA engineer&lt;/strong&gt;&lt;/a&gt;, but to reduce the distance between ‘the test failed’ and ‘this is why it probably failed.’&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Problem With Traditional Test Failure Analysis&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A typical Playwright test might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test("user can complete checkout", async ({ page }) =&amp;gt; { 

await page.goto("/checkout"); 

await page.getByRole("button", { 

name: "Place Order" 

}).click(); 

await expect( 

page.getByText("Order confirmed") 

).toBeVisible(); 

});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose the test fails. The CI system might only show: &lt;/p&gt;

&lt;p&gt;TimeoutError: &lt;/p&gt;

&lt;p&gt;"Order confirmed" was not visible &lt;/p&gt;

&lt;p&gt;The underlying cause could exist at several layers:&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%2Fewhwsdigjzirrkc77hjt.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%2Fewhwsdigjzirrkc77hjt.png" alt=" " width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A useful failure-analysis system therefore needs more context than the final assertion message. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Turning Playwright Into an Evidence Source&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Playwright already provides many of the artifacts required for deeper diagnosis. &lt;/p&gt;

&lt;p&gt;Its Trace Viewer can expose the action timeline, DOM snapshots, screenshots, console output, network activity, source information, and other metadata associated with a test execution. &lt;/p&gt;

&lt;p&gt;Instead of treating a failed test as: &lt;/p&gt;

&lt;p&gt;PASS / FAIL &lt;/p&gt;

&lt;p&gt;we can treat it as:&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%2Fo3dass5yp7elbg8ky3aa.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%2Fo3dass5yp7elbg8ky3aa.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For CI, Playwright supports collecting traces on retry and screenshots when failures occur, providing useful debugging information without collecting the full set of diagnostic artifacts for every successful test.&lt;/p&gt;

&lt;p&gt;A practical configuration is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { defineConfig } from "@playwright/test"; 

export default defineConfig({ 

retries: 1, 

use: { 

trace: "on-first-retry", 

screenshot: "only-on-failure", 

video: "retain-on-failure" 

} 

});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a much richer failure package.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The AI-Assisted Failure Analysis Architecture&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The proposed architecture separates execution from reasoning.&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%2Fp783jhl43mzrvhy7vy4s.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%2Fp783jhl43mzrvhy7vy4s.png" alt=" " width="800" height="950"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This architecture keeps Playwright's role deterministic.&lt;/p&gt;

&lt;p&gt;AI does not decide whether a test passes or fails. Playwright makes that decision. AI helps explain why the failure may have happened. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Building a Structured Failure Package&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Sending an entire CI workspace to an &lt;a href="https://geekyants.com/blog/ai-in-business-custom-models-for-scalable-innovation" rel="noopener noreferrer"&gt;&lt;strong&gt;AI model&lt;/strong&gt;&lt;/a&gt; would be inefficient and potentially unsafe. Instead, the pipeline can create a structured failure object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface FailureEvidence { 

testName: string; 

file: string; 

error: string; 

url?: string; 

browser?: string; 

duration?: number; 

consoleErrors: string[]; 

networkFailures: string[]; 

screenshotPath?: string; 

tracePath?: string; 

}
&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;{ 

"testName": "checkout payment", 

"file": "checkout.spec.ts", 

"error": "Order confirmation not visible", 

"url": "/checkout", 

"browser": "chromium", 

"consoleErrors": [], 

"networkFailures": [ 

"POST /api/payment -&amp;gt; 500" 

] 

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI now has structured context instead of receiving only:&lt;/p&gt;

&lt;p&gt;Test failed. &lt;/p&gt;

&lt;p&gt;Why? &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What the AI Should Analyze&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The AI layer can classify failures into a controlled set of categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Application &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test Data &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unknown &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The analysis flow becomes:&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%2Fsmqm1sbc5r1qjtwys33l.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%2Fsmqm1sbc5r1qjtwys33l.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A structured prompt can encourage a consistent output format:&lt;/p&gt;

&lt;p&gt;Analyze the Playwright failure using only the supplied evidence. Classify the failure as:&lt;br&gt;&lt;br&gt;
APPLICATION&lt;br&gt;&lt;br&gt;
TEST&lt;br&gt;&lt;br&gt;
NETWORK&lt;br&gt;&lt;br&gt;
DATA&lt;br&gt;&lt;br&gt;
ENVIRONMENT&lt;br&gt;&lt;br&gt;
UNKNOWN&lt;/p&gt;

&lt;p&gt;Return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Classification&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Probable root cause&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Supporting evidence&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Confidence&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recommended next investigation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not invent information.&lt;br&gt;&lt;br&gt;
Do not modify the test.&lt;br&gt;&lt;br&gt;
Do not modify application code.&lt;/p&gt;

&lt;p&gt;The AI could then return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
"classification": "APPLICATION",
"rootCause": "Payment API returned HTTP 500",
"confidence": 0.91, "evidence": [ "POST /api/payment returned 500",
"Checkout page loaded successfully"
],
"nextStep": "Inspect payment service logs"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction between evidence and AI interpretation is important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observed Evidence&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;≠&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;AI Hypothesis&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Example: Diagnosing a Checkout Failure&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Imagine that Playwright reports: &lt;/p&gt;

&lt;p&gt;Timeout: &lt;/p&gt;

&lt;p&gt;"Order confirmed" was not visible &lt;/p&gt;

&lt;p&gt;A conventional debugging process might look like: &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%2Fuy5sifhivbx51fmprp0k.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%2Fuy5sifhivbx51fmprp0k.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://geekyants.com/blog/how-ai-ml-are-transforming-quality-assurance-in-software-testing-with-playwright-examples" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-assisted workflow&lt;/strong&gt;&lt;/a&gt; can consolidate this: &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%2Fkky20nm9g6s2bamf16bi.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%2Fkky20nm9g6s2bamf16bi.png" alt=" " width="800" height="817"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The engineer still validates the diagnosis, but the investigation starts with a more focused hypothesis.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Why AI Should Not Automatically Fix the Test&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;An obvious extension would be: &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%2Fbrzmceoyce4w4kcfceo3.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%2Fbrzmceoyce4w4kcfceo3.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can create a serious testing risk. Suppose the application introduces a genuine regression, but the AI incorrectly concludes that the locator is outdated. It changes the test. The test passes. The application defect remains. &lt;/p&gt;

&lt;p&gt;A safer architecture is: &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%2Fqrk7cudl1aetsinyenxk.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%2Fqrk7cudl1aetsinyenxk.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI therefore becomes a diagnostic assistant rather than an autonomous test maintainer.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Handling Security and Sensitive Test Data&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Failure artifacts can contain &lt;a href="https://geekyants.com/security-and-compliance" rel="noopener noreferrer"&gt;&lt;strong&gt;sensitive information&lt;/strong&gt;&lt;/a&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Access tokens &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cookies &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User information &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API payloads &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authorization headers &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Screenshots &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, evidence should be sanitized before it reaches the &lt;a href="https://geekyants.com/ai" rel="noopener noreferrer"&gt;&lt;strong&gt;AI service&lt;/strong&gt;&lt;/a&gt;. A simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function sanitize(value: string): string { 

return value 

.replace( 

/Bearer\s+\S+/g, 

"Bearer [REDACTED]" 

) 

.replace( 

/password=\S+/gi, 

"password=[REDACTED]" 

); 

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete flow becomes:&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%2Fovsdu0ef27foapc3txq0.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%2Fovsdu0ef27foapc3txq0.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For production implementations, sanitization should be treated as part of the architecture rather than an optional enhancement. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Measuring Whether AI Actually Helps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A key part of evaluating this system is experimentation.&lt;/p&gt;

&lt;p&gt;Producing an AI-generated summary is straightforward. The more useful question is whether the system reduces engineering effort while maintaining acceptable diagnostic accuracy.&lt;/p&gt;

&lt;p&gt;A controlled experiment could contain: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;10 locator failures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;10 API failures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;10 network failures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;10 data failures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;10 environment failures&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then compare: &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;&lt;col&gt;&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Metric Traditional AI-Assisted&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Mean diagnosis time Measure Measure&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Correct classification Measure Measure&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Incorrect classification Measure Measure&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Human intervention Measure Measure&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Analysis latency Measure Measure&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One particularly useful metric is Mean Time to Diagnose (MTTD). For example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Failure → Investigation → Root Cause&lt;/p&gt;

&lt;p&gt;18 min&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-assisted:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Failure → AI Analysis → Validation → Root Cause&lt;/p&gt;

&lt;p&gt;5 min  2 min&lt;/p&gt;

&lt;p&gt;These numbers should come from the actual experiment rather than assumptions. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Failure Modes of AI-Assisted Diagnosis&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AI does not eliminate debugging problems. It introduces another layer of possible failure. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The system may classify an application defect as a test defect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It may generate a plausible explanation when insufficient evidence exists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It may infer behavior that was never present in the application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It may become overconfident when the available evidence is incomplete.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, the system should support an explicit unknown state: &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%2Ft2tm6j08wda2aet1re3r.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%2Ft2tm6j08wda2aet1re3r.png" alt=" " width="800" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A system that says “insufficient evidence” is often safer than one that confidently invents a root cause. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Production-Ready Pipeline&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A production implementation can evolve into:&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%2F3uvie9ls554xgqcwi953.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%2F3uvie9ls554xgqcwi953.png" alt=" " width="800" height="1150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This architecture can also be extended with historical test results, defect-management systems, CI metadata, and &lt;a href="https://geekyants.com/engineering/devops/observability-assessment-services" rel="noopener noreferrer"&gt;&lt;strong&gt;application logs&lt;/strong&gt;&lt;/a&gt;. As the evidence pipeline becomes more reliable, the AI analysis can provide more useful diagnoses.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Automated testing addresses a major part of software quality workflows by executing tests consistently at scale.&lt;/p&gt;

&lt;p&gt;The next challenge is understanding failures efficiently. &lt;/p&gt;

&lt;p&gt;An AI-assisted Playwright failure analysis pipeline addresses this by combining:&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%2Fl2zqvd2v3qhrtcvc85o4.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%2Fl2zqvd2v3qhrtcvc85o4.png" alt=" " width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key is not to position AI as a replacement for Playwright or QA engineers. &lt;/p&gt;

&lt;p&gt;Playwright should remain responsible for deterministic test execution. AI should focus on interpreting evidence, correlating signals, classifying failures, and suggesting where engineers should investigate. &lt;/p&gt;

&lt;p&gt;The real measure of success is therefore not:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;“Can AI explain a failed test?”&lt;/em&gt;&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;It is:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;em&gt;“Can AI measurably reduce the time engineers spend moving from a test failure to a validated root cause without introducing unacceptable diagnostic errors?”&lt;/em&gt;&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;That is a problem that can be tested, measured, and improved. This makes AI-assisted failure analysis a practical engineering problem rather than another demonstration of generative AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;From Test Failure to Faster Diagnosis&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AI-assisted failure analysis is most useful when it strengthens the testing workflow without replacing deterministic execution or human judgment. By combining Playwright evidence with structured AI analysis and human validation, teams can investigate failures with clearer context and make informed decisions about the next step. For teams looking to extend this approach across automated testing and continuous validation, explore GeekyAnts’ &lt;a href="https://geekyants.com/engineering/quality-assurance/automation-testing-services" rel="noopener noreferrer"&gt;&lt;strong&gt;Automation Testing Services&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Automating Everything: A Balanced Quality Engineering Approach to Testing</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:55:20 +0000</pubDate>
      <link>https://dev.to/geekyants/stop-automating-everything-a-balanced-quality-engineering-approach-to-testing-1h76</link>
      <guid>https://dev.to/geekyants/stop-automating-everything-a-balanced-quality-engineering-approach-to-testing-1h76</guid>
      <description>&lt;p&gt;Balanced quality engineering places automation, API testing, exploratory work, and AI where each gives the most value, so teams ship faster without trading away user-perceived quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Key Takeaways&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automate by Risk&lt;/strong&gt;: Focus automation on &lt;strong&gt;business-critical, high-risk workflows&lt;/strong&gt; rather than raw test-case count, since more tests do not mean better quality.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cheapest Effective Layer&lt;/strong&gt;: Validate business logic through &lt;a href="https://geekyants.com/engineering/quality-assurance/api-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;API tests&lt;/strong&gt;&lt;/a&gt; where possible and reserve &lt;strong&gt;UI automation&lt;/strong&gt; for the user journeys that matter most.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep Exploratory Testing&lt;/strong&gt;: Human testers surface &lt;strong&gt;unexpected behaviour&lt;/strong&gt;, &lt;strong&gt;usability gaps&lt;/strong&gt;, and &lt;strong&gt;edge cases&lt;/strong&gt; that predefined tests never anticipate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure Quality, Not Volume&lt;/strong&gt;: Track &lt;strong&gt;defect detection&lt;/strong&gt;, &lt;strong&gt;leakage&lt;/strong&gt;, &lt;strong&gt;flakiness&lt;/strong&gt;, &lt;strong&gt;maintenance effort&lt;/strong&gt;, and &lt;strong&gt;investigation time&lt;/strong&gt; instead of counting automated tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://geekyants.com/ai-accelerator" rel="noopener noreferrer"&gt;&lt;strong&gt;AI as an Accelerator&lt;/strong&gt;&lt;/a&gt;: Let AI assist with &lt;strong&gt;test generation&lt;/strong&gt;, &lt;strong&gt;failure analysis&lt;/strong&gt;, and &lt;strong&gt;maintenance&lt;/strong&gt; while engineers stay responsible for &lt;strong&gt;quality decisions&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Passing QA Tests Can Still Ship Poor Experiences&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Software teams are expected to release faster while maintaining reliable digital experiences. Automation has become a critical part of achieving this goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A typical automated regression workflow looks like:&lt;/strong&gt;&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%2Fh8qbg1lla1qoiucyb199.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%2Fh8qbg1lla1qoiucyb199.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This provides fast and repeatable feedback, but passing tests do not necessarily mean that users are experiencing a high-quality product.&lt;/p&gt;

&lt;p&gt;For example, an automated checkout test may verify that an order is successfully created while missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;confusing error messages,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;poor mobile usability,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;inaccessible controls,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;unclear navigation,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;unexpected user workflows.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical correctness and user-perceived quality are related, but they are not the same thing.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Moving From Test Automation to Quality Engineering&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Traditional testing often follows:&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%2Fctpj4si8yluy9p2ytme2.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%2Fctpj4si8yluy9p2ytme2.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://geekyants.com/service/hire-quality-assurance-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;Quality Engineering&lt;/strong&gt;&lt;/a&gt; approach distributes quality throughout the lifecycle:&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%2Fudcxyopiajexqwb3traa.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%2Fudcxyopiajexqwb3traa.png" alt=" " width="800" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal is not simply to find more bugs. It is to provide fast, reliable quality feedback at the appropriate layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What a Balanced Testing Model Looks Like&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A balanced QE strategy can combine five capabilities:&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%2F6968goizfjhtx93r1ptn.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%2F6968goizfjhtx93r1ptn.png" alt=" " width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each layer has a different purpose.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Testing approach&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Primary purpose&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Unit testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate isolated logic&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;API testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate services and business rules&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UI automation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate critical user journeys&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Exploratory testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Discover unexpected behavior&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UX testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate the user experience&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;AI assistance&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Accelerate testing activities&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Deciding What to Automate&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Not every scenario should become a &lt;a href="https://geekyants.com/engineering/quality-assurance/automation-testing-services" rel="noopener noreferrer"&gt;&lt;strong&gt;UI automation test&lt;/strong&gt;&lt;/a&gt;. Strong automation candidates are generally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;repeatable,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;deterministic,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;frequently executed,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;business-critical,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;expensive to execute manually.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Login&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Checkout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Order creation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Permission validation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API contracts&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful decision process is:&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%2Fn6pfkc9jizt1maiejvx2.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%2Fn6pfkc9jizt1maiejvx2.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The objective should not be 100% automation.&lt;/p&gt;

&lt;p&gt;It should be meaningful risk coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Validate Business Rules Through APIs, Not the UI&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One common mistake is validating every business rule through the UI.&lt;/p&gt;

&lt;p&gt;Suppose an application exposes:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The business logic can be validated directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await request.post("/api/orders", {
  data: {
    productId: 123,
    quantity: 2
  }
});

expect(response.status()).toBe(201);

const body = await response.json();

expect(body.quantity).toBe(2);
expect(body.status).toBe("created");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI test can then focus on whether a user can successfully complete the workflow. This creates a useful separation:&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%2F967nvv5ruglqco7x1fb8.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%2F967nvv5ruglqco7x1fb8.png" alt=" " width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This can reduce unnecessary UI automation and make regression suites faster and easier to maintain.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Human Testers Catch That Automation Misses&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Automation executes what engineers explicitly define.&lt;/p&gt;

&lt;p&gt;Humans can investigate what was not anticipated.&lt;/p&gt;

&lt;p&gt;For example, an exploratory checkout charter might include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore checkout with focus on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;invalid inputs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;browser back button&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;session expiry&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;duplicate submission&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;slow network&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mobile viewport&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;unexpected navigation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tester might discover:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Field&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Observation&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Action&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Submit payment twice quickly.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Expected&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;One order.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Actual&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Two orders created.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Risk&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Duplicate order/payment.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A standard happy-path automation test might never discover this scenario. This is why &lt;a href="https://geekyants.com/engineering/quality-assurance/manual-testing-services" rel="noopener noreferrer"&gt;&lt;strong&gt;exploratory testing&lt;/strong&gt;&lt;/a&gt; should not disappear when automation increases.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why a Green Suite Isn't Proof of a Good Experience&lt;/strong&gt;
&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;await page.getByRole("button", {
  name: "Place Order"
}).click();

await expect(
  page.getByText("Order confirmed")
).toBeVisible();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test proves that the order can be placed.&lt;/p&gt;

&lt;p&gt;It does not necessarily prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the button is easy to find,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the error messages are understandable,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the workflow is accessible,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the mobile experience is usable,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the customer understands the payment status.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where human-centered testing complements automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Metrics That Show Whether the Strategy Works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A balanced QE strategy should be measurable.&lt;/p&gt;

&lt;p&gt;Instead of measuring only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Number of automated tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;track metrics such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Execution time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Defects detected&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Defect leakage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flaky tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintenance effort&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Failure investigation time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exploratory findings&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical experiment can use a fixed set of application workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Registration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Login&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Product Search&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cart&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Checkout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Order Confirmation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Invoice Download&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then evaluate them using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;UI Automation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API Testing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exploratory Testing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;UX-focused Testing&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The experiment should record the same measures for each testing approach. Until real results are available, it is better to define the measurement plan rather than show placeholder values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Metric&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;What to record&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Execution time&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Time required to complete the selected workflow or test set&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Unique defects detected&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Distinct defects found by each testing approach&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Maintenance effort&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Time spent updating or repairing tests as the application changes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Investigation time&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Time required to understand and classify a failed test or observed issue&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Defect leakage&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Defects that escaped earlier test layers and were found later&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Flaky-test rate&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Intermittent automated failures that do not consistently reproduce&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Exploratory findings&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Unexpected behaviors, usability problems, or risk scenarios discovered through human investigation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Charts Worth Publishing Once You Have Real Data&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once the experiment has produced real measurements, two graphs can communicate the results clearly without making the article visually heavy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Execution Time by Testing Approach&lt;/strong&gt; — compare the measured execution time for UI automation, API testing, exploratory testing, and UX-focused testing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unique Defects Detected by Testing Approach&lt;/strong&gt; — compare the number of distinct defects found by each approach, ideally with defect categories called out where useful.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not publish these charts with illustrative or fabricated values. Generate them only from the completed experiment and label the units, sample size, and test conditions so readers can interpret the results correctly.&lt;/p&gt;

&lt;p&gt;These graphs should reinforce an important point:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different testing approaches detect different categories of risk.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Keeping Automation Reliable Enough to Trust&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Automation becomes less valuable when teams stop trusting test results.&lt;/p&gt;

&lt;p&gt;A test that behaves like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run 1 → PASS
Run 2 → PASS
Run 3 → FAIL
Run 4 → PASS
Run 5 → PASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is potentially flaky.&lt;/p&gt;

&lt;p&gt;Avoid solving this by adding arbitrary waits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.waitForTimeout(5000);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, synchronize with application state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await expect(
  page.locator('[data-testid="order-confirmation"]')
).toBeVisible();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The principle is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wait for conditions, not time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Automation should be treated as production-quality software, with code review, maintainability, monitoring, and refactoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where AI Helps — and Where It Shouldn't Decide&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AI can extend the QE workflow without replacing deterministic testing.&lt;/p&gt;

&lt;p&gt;A practical architecture is:&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%2Fam0iv899njf26rp8jb2c.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%2Fam0iv899njf26rp8jb2c.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, AI can analyze:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://geekyants.com/blog/how-ai-ml-are-transforming-quality-assurance-in-software-testing-with-playwright-examples" rel="noopener noreferrer"&gt;&lt;strong&gt;Playwright Trace&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Screenshot&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DOM&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Console Logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network Errors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test History&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and suggest a failure category:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Test defect&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application defect&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment issue&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data issue&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network issue&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the engineer should validate the result.&lt;/p&gt;

&lt;p&gt;A useful operating model is:&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%2Fl7k5jdihsf933ispr71o.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%2Fl7k5jdihsf933ispr71o.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This keeps AI as an accelerator rather than an uncontrolled decision-maker.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Fitting the Approach Into Your CI/CD Pipeline&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The balanced approach fits naturally into a modern CI/CD pipeline:&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%2F7jwcq6orqmixy6tdao3o.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%2F7jwcq6orqmixy6tdao3o.png" alt=" " width="800" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Different test layers provide feedback at different speeds, allowing teams to avoid using expensive UI tests for problems that can be detected earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Trade-Offs Between Testing Approaches&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There is no single testing technique that covers every quality dimension.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Strength&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Limitation&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UI Automation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;End-to-end regression&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Maintenance cost&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;API Testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Fast business validation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Doesn't validate UX&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Exploratory&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Finds unexpected issues&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Less repeatable&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UX Testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;User experience&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Requires human effort&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;AI Testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Faster analysis/generation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Requires validation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The right strategy combines these capabilities according to product risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Reference Architecture You Can Adapt&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The complete model can be summarized as:&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%2Fe2xhz9rc8n7nfezsortf.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%2Fe2xhz9rc8n7nfezsortf.png" alt=" " width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This model does not attempt to eliminate human testing.&lt;/p&gt;

&lt;p&gt;Instead, it places automation, APIs, AI, and human reasoning where each provides the most value.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion: Getting the Balance Right&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Modern Quality Engineering should not be a competition between manual testing and automation.&lt;/p&gt;

&lt;p&gt;Automation provides speed, repeatability, and scalable regression coverage. API testing provides efficient validation of business behavior. Exploratory and human-centered testing provide contextual reasoning and help identify problems that deterministic automation may miss. AI can further accelerate these activities by assisting with generation, analysis, and maintenance.&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%2Fs6u897n0iu5sjoh1g6fc.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%2Fs6u897n0iu5sjoh1g6fc.png" alt=" " width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most effective Quality Engineering strategy is one that uses technology to accelerate testing without losing sight of the person ultimately using the software.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is not maximum automation. The goal is maximum meaningful quality feedback for the engineering effort invested.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Rolling Deployments to Zero-Downtime Releases</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:17:22 +0000</pubDate>
      <link>https://dev.to/geekyants/from-rolling-deployments-to-zero-downtime-releases-k55</link>
      <guid>https://dev.to/geekyants/from-rolling-deployments-to-zero-downtime-releases-k55</guid>
      <description>&lt;p&gt;Executive Summary&lt;br&gt;
In &lt;a href="https://geekyants.com/industry/fintech-app-development-services" rel="noopener noreferrer"&gt;digital banking&lt;/a&gt;, deployment availability is a business requirement as much as a technical one. A deployment that interrupts access to online banking can affect customers attempting to log in, view balances, initiate transfers, or perform other critical operations.&lt;/p&gt;

&lt;p&gt;Our earlier rolling deployment model introduced problems around version coexistence. Every new frontend build generated build-specific resources such as runtime.1234.js and main.1234.js. During a rolling restart, old and new pods could coexist. A request for a resource from the new build could reach an older pod where that resource did not exist, producing a 404 response.&lt;/p&gt;

&lt;p&gt;Static resources were cached at the CDN for about four minutes. We needed to consider application availability and version consistency across the application shell, static resources, CDN, and backend pods.&lt;/p&gt;

&lt;p&gt;We moved to a &lt;a href="https://geekyants.com/engineering/devops/infrastructure-deployment-automation" rel="noopener noreferrer"&gt;Blue-Green deployment strategy&lt;/a&gt;. The new version is deployed into a separate environment, its pods start and pass readiness checks, and the environment is validated before customer traffic is switched. The existing environment continues serving production traffic until the cutover.&lt;/p&gt;

&lt;p&gt;In our environment, the observed ALB traffic transition from Blue to Green was about 40–180 ms. The key benefit is not faster application startup. Pod creation, image pulling, startup, and readiness occur outside the customer-facing cutover window. The previous environment also remains available so that traffic can be routed back to it if a rollback is required.&lt;/p&gt;

&lt;p&gt;A race condition remained during the ALB transition under high request volumes. A request for a build-specific static resource could reach the previous environment and receive a 404 response. We addressed this with a targeted NGINX retry for the affected static resources, with a delay of about one second before the retry. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why We Needed a Different Deployment Strategy
1.1 The Rolling Deployment Model
A rolling deployment replaces application instances in stages. This avoids replacing every instance at once, but it also means different application versions can coexist during the deployment.&lt;/li&gt;
&lt;/ol&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%2Fmfi77xxm905wazjq67jq.jpg" 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%2Fmfi77xxm905wazjq67jq.jpg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;1.2 Build-Specific Static Resources&lt;br&gt;
Every application build generates resources associated with that build. For example:&lt;/p&gt;

&lt;p&gt;Build 100:&lt;br&gt;
runtime.100.js&lt;br&gt;
main.100.js&lt;br&gt;
styles.100.css&lt;/p&gt;

&lt;p&gt;Build 101:&lt;br&gt;
runtime.101.js&lt;br&gt;
main.101.js&lt;br&gt;
styles.101.css&lt;/p&gt;

&lt;p&gt;The browser receives an application shell that references a particular build's resources. If a request for runtime.101.js reaches a Build 100 pod, the resource does not exist there, and the server can return a 404 response.&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%2Fg5tu5ww0suaxps639n0y.jpg" 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%2Fg5tu5ww0suaxps639n0y.jpg" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pod itself could be healthy. The failure was caused by a version mismatch between the resource requested by the client and the application instance handling the request.&lt;/p&gt;

&lt;p&gt;1.3 CDN Caching Added Another Timing Dimension&lt;br&gt;
Static resources were cached at the CDN for about four minutes. The application origin could move to a new version while cached resources from the previous version remained available at the edge.&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%2F8kqslrxakk8cphsda3vv.jpg" 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%2F8kqslrxakk8cphsda3vv.jpg" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deployment correctness had to account for three independent timing domains: application and pod readiness, ALB traffic convergence, and CDN cache lifetime.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Blue-Green Deployment Architecture
Blue-Green deployment introduced two complete application environments. Blue is the active version, and Green is the new version.&lt;/li&gt;
&lt;/ol&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%2Fsz6btqq06tq9diupfw32.jpg" 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%2Fsz6btqq06tq9diupfw32.jpg" alt=" " width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The principle is simple: deploy and validate Green before allowing it to receive production traffic. This separates deployment preparation from production traffic cutover.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What Happens During a Blue-Green Deployment
3.1 Build and Image Creation
The CI/CD pipeline creates an immutable image for the release. The frontend resources generated by that build are part of the same release artifact.&lt;/li&gt;
&lt;/ol&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%2F70lj155zhmj21yjdva1l.jpg" 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%2F70lj155zhmj21yjdva1l.jpg" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This preserves a clear relationship between the application version and the resources it serves.&lt;/p&gt;

&lt;p&gt;3.2 Creating the Green Pods&lt;br&gt;
The new release is deployed to Green without terminating the active Blue environment.&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%2Fks32weg5k9mul9f4gmdj.jpg" 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%2Fks32weg5k9mul9f4gmdj.jpg" alt=" " width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Green pods go through image pull, container startup, application initialization, and readiness checks before they become eligible to receive traffic.&lt;/p&gt;

&lt;p&gt;3.3 Pod Readiness&lt;br&gt;
Readiness is a key mechanism in this architecture. &lt;a href="https://geekyants.com/engineering/devops/kubernetes-containerization-services" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; distinguishes a running container from a container that is ready to accept traffic. A failed readiness probe causes the pod to be treated as not ready for normal service traffic. A startup probe can be used when an application needs more initialization time before liveness or readiness checks begin.&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%2Fz3h5kidyck7u46ek98ps.jpg" 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%2Fz3h5kidyck7u46ek98ps.jpg" alt=" " width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a banking application, readiness should represent the minimum state in which the application can process production requests without creating service risk. Depending on the implementation, this can include application initialization, configuration loading, dependency availability, and other required startup conditions.&lt;/p&gt;

&lt;p&gt;3.4 Green Validation&lt;br&gt;
Once the Green pods are Ready, the new environment can be validated before customer traffic is moved.&lt;/p&gt;

&lt;p&gt;Confirm that the pods and application are healthy.&lt;br&gt;
Run API &lt;a href="https://geekyants.com/service/hire-quality-assurance-developers" rel="noopener noreferrer"&gt;smoke tests&lt;/a&gt; against the new environment.&lt;br&gt;
Confirm that the required static resources are available.&lt;br&gt;
Check connectivity to critical dependencies.&lt;br&gt;
Test business-critical flows.&lt;br&gt;
Review application and infrastructure metrics.&lt;br&gt;
This allows the team to validate Green while Blue continues serving production traffic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How the ALB Traffic Switch Works
The Application Load Balancer (ALB) provides the boundary for switching production traffic between Blue and Green. An ALB listener evaluates routing rules and forwards traffic to target groups. Target groups contain registered targets and can have their own health checks.&lt;/li&gt;
&lt;/ol&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%2Faqg5nht69berba4bp160.jpg" 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%2Faqg5nht69berba4bp160.jpg" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before the cutover, Blue is the active target group and Green is prepared without receiving normal production traffic. Once Green has passed the deployment gates, the routing configuration changes so that production requests are directed to Green.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Critical Difference: Deployment Time vs Customer-Impact Time
A key distinction in Blue-Green deployment is the difference between deployment time and customer-impact time. The new pods still need to pull the image, start the container, initialize the application, and pass readiness checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What changes is where that work occurs.&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%2Fle45h9s4sb6mdn5euomh.jpg" 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%2Fle45h9s4sb6mdn5euomh.jpg" alt=" " width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blue continues serving customers while Green is being prepared. This removes the time-consuming preparation work from the customer-facing cutover path.&lt;/p&gt;

&lt;p&gt;The customer-facing transition is limited to the routing window rather than the full duration required to create and warm the new pods.&lt;/p&gt;

&lt;p&gt;The improvement should not be described as “the deployment became X% faster.” The deployment preparation time still exists, but it does not need to translate into customer downtime.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rollback: Turning Recovery Into a Traffic Switch
Blue-Green changes the rollback mechanism.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Normal state:&lt;/p&gt;

&lt;p&gt;BLUE  = Build 100&lt;br&gt;
GREEN = Build 101&lt;br&gt;
Traffic = 100% Green&lt;br&gt;
If monitoring identifies a production problem after the cutover, Blue can remain available as the previous known-good version.&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%2Fg1n070keblg11p57vl64.jpg" 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%2Fg1n070keblg11p57vl64.jpg" alt=" " width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of rebuilding and redeploying the previous image, the deployment can route traffic back to the running Blue environment.&lt;/p&gt;

&lt;p&gt;This changes rollback from a deployment operation into a traffic-routing operation, which can reduce mean time to recovery (MTTR).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Production Challenge After Blue-Green
7.1 The ALB Transition Window
Blue-Green solved the major version-coexistence problem, but production testing exposed a smaller race condition during the traffic switch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In our environment, the observed ALB transition from Blue to Green was about 40–180 ms.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
Blue  = 100%&lt;br&gt;
Green =   0%&lt;/p&gt;

&lt;p&gt;Transition:&lt;br&gt;
Blue  = 100% -&amp;gt; 0%&lt;br&gt;
Green =   0% -&amp;gt; 100%&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
Blue  =   0%&lt;br&gt;
Green = 100%&lt;br&gt;
Under high request volume, requests can arrive during this short interval. A request that arrives during the transition can encounter the previous routing state.&lt;/p&gt;

&lt;p&gt;7.2 Static Resource 404 During the Transition&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%2Fa2mnpiem2dl2q8wiypxy.jpg" 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%2Fa2mnpiem2dl2q8wiypxy.jpg" alt=" " width="799" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was not a persistent application failure. It was a transient race condition caused by the interaction between build-specific resource names and traffic convergence.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;NGINX Retry as the Last-Mile Resilience Layer
To address this transient condition, a targeted retry mechanism was introduced at NGINX for the affected static resources.&lt;/li&gt;
&lt;/ol&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%2Fawj8seizzl3cgd9u1ix8.jpg" 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%2Fawj8seizzl3cgd9u1ix8.jpg" alt=" " width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one-second delay was longer than the observed ALB transition window. This gave the traffic transition time to complete before the retry.&lt;/p&gt;

&lt;p&gt;This mechanism was scoped to static-resource requests. A generic retry policy should not be applied to state-changing banking operations such as fund transfers without idempotency and transaction-safety guarantees.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CDN and Resource Versioning
Build-specific resource names remain part of the design. Instead of overwriting a stable path such as /main.js, each build produces a unique resource path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;/main.100.js&lt;br&gt;
/main.101.js&lt;br&gt;
/main.102.js&lt;br&gt;
This allows multiple versions of a resource to coexist in the CDN. The application shell should reference the resource set belonging to its build.&lt;/p&gt;

&lt;p&gt;The CDN cache lifetime of about four minutes is treated as a separate concern from the ALB transition. Build-specific resources provide coexistence, while the NGINX retry addresses the short traffic-convergence race.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zero-Downtime as an End-to-End Property
The implementation showed that zero-downtime (ZDT) is not a property of Kubernetes or the load balancer alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Client&lt;br&gt;
  |&lt;br&gt;
CDN&lt;br&gt;
  |&lt;br&gt;
ALB&lt;br&gt;
  |&lt;br&gt;
NGINX&lt;br&gt;
  |&lt;br&gt;
Kubernetes&lt;br&gt;
  |&lt;br&gt;
Pod readiness&lt;br&gt;
  |&lt;br&gt;
Application&lt;br&gt;
  |&lt;br&gt;
Dependencies / data layer&lt;/p&gt;

&lt;p&gt;A system can have healthy pods and still return customer-visible errors if resource versioning, caching, or routing transitions are not handled correctly.&lt;/p&gt;

&lt;p&gt;Our final approach combines several mechanisms:&lt;/p&gt;

&lt;p&gt;Blue-Green deployment&lt;br&gt;
        +&lt;br&gt;
Pod readiness&lt;br&gt;
        +&lt;br&gt;
Build-specific static resources&lt;br&gt;
        +&lt;br&gt;
CDN caching strategy&lt;br&gt;
        +&lt;br&gt;
ALB traffic cutover&lt;br&gt;
        +&lt;br&gt;
NGINX static-resource retry&lt;br&gt;
        +&lt;br&gt;
Fast rollback&lt;br&gt;
        =&lt;br&gt;
Zero-Downtime Release Architecture&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Business and Operational Impact
Before the move toward ZDT, deployments were scheduled during low-traffic periods such as night-time maintenance windows. This was a risk-management decision. If a deployment could cause service interruption, a period with fewer customers using the platform reduced the number of users exposed to that interruption.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before:&lt;br&gt;
Release Ready&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Wait for Night&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Maintenance Window&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Deployment&lt;/p&gt;

&lt;p&gt;Blue-Green changed the release model.&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
Release Ready&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Deploy Green&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Validate&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Switch Traffic&lt;/p&gt;

&lt;p&gt;Because Blue remains available while Green is prepared, a new deployment does not require the application to be taken offline. Subject to change-management, monitoring, and business controls, teams can perform deployments during normal operating hours, including the morning, instead of restricting them to night-time maintenance windows to avoid deployment downtime.&lt;/p&gt;

&lt;p&gt;This changes the operational model: application availability no longer dictates the deployment window.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deployment Sequence&lt;/li&gt;
&lt;li&gt;Developer merges release
      |&lt;/li&gt;
&lt;li&gt;CI/CD builds application
      |&lt;/li&gt;
&lt;li&gt;Build-specific static resources generated
      |&lt;/li&gt;
&lt;li&gt;Immutable container image created
      |&lt;/li&gt;
&lt;li&gt;Image pushed to registry
      |&lt;/li&gt;
&lt;li&gt;Green deployment created
      |&lt;/li&gt;
&lt;li&gt;Green pods start
      |&lt;/li&gt;
&lt;li&gt;Startup / initialization
      |&lt;/li&gt;
&lt;li&gt;Readiness probes pass
      |&lt;/li&gt;
&lt;li&gt;Green validation / smoke tests
      |&lt;/li&gt;
&lt;li&gt;Green target group confirmed healthy
      |&lt;/li&gt;
&lt;li&gt;ALB routing switched
      |&lt;/li&gt;
&lt;li&gt;Traffic moves Blue -&amp;gt; Green
      |&lt;/li&gt;
&lt;li&gt;Monitor application + business metrics
      |&lt;/li&gt;
&lt;li&gt;Keep Blue available for rollback
      |&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Decommission Blue after confidence window&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Key Engineering Lessons&lt;br&gt;
13.1 Blue-Green Requires Coordinated Deployment Controls&lt;br&gt;
Blue-Green depends on the interaction between deployment orchestration, readiness, traffic management, resource versioning, CDN behavior, observability, and rollback. Two application environments alone do not address the dependencies across the release path.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;13.2 Move Preparation Work Outside the Cutover Path&lt;br&gt;
Image pulling, pod creation, startup, and readiness can take minutes. Blue-Green allows the previous version to continue serving traffic while the new environment completes this work.&lt;/p&gt;

&lt;p&gt;13.3 Rollback Should Be Designed Up Front&lt;br&gt;
Keeping the previous environment available allows the system to route traffic back to a known-good version without rebuilding and redeploying that version.&lt;/p&gt;

&lt;p&gt;13.4 Short Transition Windows Can Expose Race Conditions&lt;br&gt;
A short routing transition can still expose a race condition when request volume is high. Deployment design must account for requests that arrive while traffic moves between environments.&lt;/p&gt;

&lt;p&gt;13.5 Static Resources Are Part of the Release Architecture&lt;br&gt;
Frontend resources form part of the production deployment architecture. Their versioning, CDN lifetime, and relationship with the application shell must be considered as part of the release design.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Final Architecture&lt;/li&gt;
&lt;/ol&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%2F2vru39ubuumf2jcye9ld.jpg" 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%2F2vru39ubuumf2jcye9ld.jpg" alt=" " width="800" height="984"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Conclusion
The move from rolling deployments to Blue-Green deployment addressed the version mismatch that could occur when build-specific frontend resources reached pods running an older build. Blue-Green separates release preparation from traffic cutover by allowing the new environment to complete startup, readiness checks, and validation while the existing environment continues serving customers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The implementation showed that zero-downtime deployment depends on controls across the release path. Resource versioning, CDN caching, traffic switching, readiness checks, retry behavior, and rollback design must work together to protect customer requests during a release.&lt;/p&gt;

&lt;p&gt;Keeping the previous environment available also allows recovery through a traffic switch instead of another deployment. This release model reduces the need to schedule deployments around maintenance windows while preserving a path back to the previous version.&lt;/p&gt;

&lt;p&gt;Building Release Architectures for Production Reliability&lt;br&gt;
Zero-downtime deployment depends on more than switching traffic between two environments. Pod readiness, static-resource versioning, caching, traffic routing, &lt;a href="https://geekyants.com/engineering/devops" rel="noopener noreferrer"&gt;monitoring, and rollback&lt;/a&gt; must work together to protect customer-facing applications during a release. For teams building or modernizing applications with these production requirements, GeekyAnts provides &lt;a href="https://geekyants.com/service/custom-software-development-services" rel="noopener noreferrer"&gt;custom software development services&lt;/a&gt; covering architecture, CI/CD, deployment, and post-launch support.&lt;/p&gt;

&lt;p&gt;References&lt;br&gt;
Kubernetes: Liveness, Readiness, and Startup Probes&lt;br&gt;
&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/probes/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/workloads/pods/probes/&lt;/a&gt;&lt;br&gt;
AWS Elastic Load Balancing: Application Load Balancer&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html&lt;/a&gt;&lt;br&gt;
AWS Elastic Load Balancing: Target Groups&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html&lt;/a&gt;&lt;br&gt;
AWS Elastic Load Balancing: Listener Rules&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/en_en/elasticloadbalancing/latest/application/listener-rules.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/en_en/elasticloadbalancing/latest/application/listener-rules.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>banking</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The Deprecation Notice Nobody Reads</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:49:51 +0000</pubDate>
      <link>https://dev.to/geekyants/the-deprecation-notice-nobody-reads-1i96</link>
      <guid>https://dev.to/geekyants/the-deprecation-notice-nobody-reads-1i96</guid>
      <description>&lt;p&gt;This blog explains why API deprecation notices often fail and how staged retirement, telemetry, brownouts, and scheduled enforcement can make API migrations more controlled and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Every API team eventually writes the same message&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;"This endpoint will be removed on [date]. Please migrate before then." It goes in a changelog, a blog post, maybe an email. Traffic to the deprecated endpoint can show little movement until the day it is switched off, at which point support queues fill up with people who swear they never saw the notice.&lt;/p&gt;

&lt;p&gt;They may not have seen it. A warning with no consequence attached is easy for a busy team to defer. The problem with sunset timelines is not communication. Most contain nothing that creates a cost for delay until the exact day they stop being a warning and become an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The flat line before the cliff&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The pattern shows up across &lt;a href="https://geekyants.com/hire-graphql-api-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;API providers&lt;/strong&gt;&lt;/a&gt;: deprecation is announced, documentation is updated, and usage of the old endpoint barely dips. One cited account of a typical deprecation window reported that six months after announcing a sunset date, the old version was still carrying 40% of total traffic, with a team left asking why nobody had migrated.&lt;/p&gt;

&lt;p&gt;A deprecated endpoint that still returns 200 OK carries no urgency. Migration work competes with every other roadmap item, and "the thing that still technically works" reliably loses that competition until it stops working at all. A blog post is not an effective notice if ignoring it carries no immediate consequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Two timelines, two outcomes&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Timeline&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Without a strategy&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;With a strategy&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Month 0&lt;/td&gt;
&lt;td&gt;Endpoint removed, no warning&lt;/td&gt;
&lt;td&gt;Deprecation announced; Deprecation/Sunset headers added; migration guide published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 3&lt;/td&gt;
&lt;td&gt;47 integrations already broken&lt;/td&gt;
&lt;td&gt;Migration reminders sent to consumers still calling the endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 6&lt;/td&gt;
&lt;td&gt;Support queue flooded&lt;/td&gt;
&lt;td&gt;First announced brownout window, a scheduled, published maintenance-style window during which the endpoint is intentionally degraded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 9&lt;/td&gt;
&lt;td&gt;Emergency rollback underway&lt;/td&gt;
&lt;td&gt;Quota tightened for remaining consumers; brownout windows lengthened per the published schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 12&lt;/td&gt;
&lt;td&gt;Trust damaged, repeat incidents&lt;/td&gt;
&lt;td&gt;Endpoint retired, returns 410 Gone; near-zero breakage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What changes structurally is that, by month 6, the endpoint behaves differently for consumers who have not migrated. That change is published on a schedule before it takes effect rather than discovered by consumers in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why longer notice periods don't fix this alone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Some major API providers use deprecation or version-support windows measured in years, giving customers time to include migration work in planning cycles.&lt;/p&gt;

&lt;p&gt;But two years solves a planning problem, not an urgent problem. It does nothing to make migration feel pressing in month three, nine, or eighteen because nothing about the API has changed yet. The calendar moved; the consequence didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Staged, announced retirement&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The fix isn't a louder notice, and it isn't quiet degradation either. It's a retirement plan the API enforces on a schedule the consumer knows about. Every stage is disclosed in the changelog and response headers before it takes effect rather than discovered in production.&lt;/p&gt;

&lt;p&gt;A staged retirement can follow five stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Headers-only window. Deprecation and Sunset headers present on every response. No behavioral change. This window should be long enough to cover at least one full planning cycle for your consumers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Warnings surfaced in-band. A deprecation notice is added to response bodies, or a Link header points to the migration guide, so tooling rather than memory catches it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Announced brownout windows. Published, scheduled windows (e.g., "the deprecated endpoint will return elevated latency and occasional 429s every Tuesday 2–4pm UTC starting date") during which the endpoint behaves like it will after full retirement. These are calendared and documented before they start, exactly like a maintenance window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Progressively tighter quotas for remaining consumers, identified by telemetry, not by chance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hard retirement. 410 Gone, with the migration link still present.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At every stage, a consumer hitting the new behavior can look at a public schedule and confirm exactly which stage is in effect and when the next one begins. Nothing about it is a surprise, and nothing about it is silent.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What changes for internal versus public APIs&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For an internal API with a handful of known consuming teams, the friction curve can be steep and communication can be direct. A Slack message plus a dashboard showing which service is still calling the old path can help get a fix shipped within a sprint. The consuming teams are known, and a sharper cutoff is more practical because the organization also controls the fallback.&lt;/p&gt;

&lt;p&gt;Public and partner-facing APIs are the harder case. The consumer may be unknown until they show up in usage telemetry, may not follow the changelog, and may be running an integration nobody at their organization has touched since it was built. That is where headers, published schedules, and graduated enforcement provide value. They turn an anonymous, unreachable consumer's client code into something that reports its own deprecation status back to whoever looks at the logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Comparing three retirement strategies&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Communication-only&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hard sunset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Staged enforcement (brownouts)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Changelog/email notice, no behavior change until removal day&lt;/td&gt;
&lt;td&gt;Fixed date, endpoint simply stops working&lt;/td&gt;
&lt;td&gt;Published schedule of increasing, announced friction before final removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low-risk internal APIs, few known consumers&lt;/td&gt;
&lt;td&gt;Emergency/security-driven deprecations&lt;/td&gt;
&lt;td&gt;Public and partner APIs with unknown or numerous consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry-storm risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low until cutover, spikes hard at deadline&lt;/td&gt;
&lt;td&gt;High; everyone fails at once&lt;/td&gt;
&lt;td&gt;Distributed across scheduled windows; retry behavior can be observed before it's catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SLA / contractual exposure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rarely satisfies partner deprecation clauses alone&lt;/td&gt;
&lt;td&gt;Highest exposure if the date arrives early&lt;/td&gt;
&lt;td&gt;Lower; each stage documented and pausable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback strategy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trivial (nothing changed yet)&lt;/td&gt;
&lt;td&gt;Difficult; re-enabling under load is its own incident&lt;/td&gt;
&lt;td&gt;Straightforward; each stage is a &lt;a href="https://geekyants.com/blog/feature-flags-as-technical-debt-the-cleanup-nobody-schedules" rel="noopener noreferrer"&gt;&lt;strong&gt;feature flag&lt;/strong&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low upfront, high at cutover&lt;/td&gt;
&lt;td&gt;Low upfront, very high at cutover&lt;/td&gt;
&lt;td&gt;Higher upfront, spread more evenly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When it fails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumers defer migration&lt;/td&gt;
&lt;td&gt;Consumers who missed the notice get zero warning&lt;/td&gt;
&lt;td&gt;Poorly tuned severity or cadence can trigger retries without Retry-After and backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No single strategy is correct in isolation. The choice depends on how well you know your consumers, what your contracts require, and how much operational machinery you're willing to build before you start the clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Standards-correct signals&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;RFC 9745 defines Deprecation as a Structured Fields Date, not an HTTP-date string. That means the value should look like @1735689600 (an @-prefixed Unix timestamp), not the output of toUTCString(). Sunset (RFC 8594) does use an HTTP-date. Mixing the two formats can break client tooling. A malformed value may fail to parse as expected, which can disrupt automated deprecation tracking.&lt;/p&gt;

&lt;p&gt;Production-oriented middleware, with per-consumer rollout, a rollback switch, and telemetry hooks built in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Formats a JS Date/timestamp as an RFC 9745 Structured Field Date: "@" function toStructuredFieldDate(msTimestamp) { return @${Math.floor(msTimestamp / 1000)}; }

function deprecationMiddleware({ deprecationDate, sunsetDate, migrationLink, stages, // ordered array of { startsAt, mode, config } isEnforced, // (consumerId) =&amp;gt; bool — per-consumer rollout / allowlist telemetry, // { recordHit(stage, consumerId), recordOutcome(stage, consumerId, result) } killSwitch, // () =&amp;gt; bool — global rollback }) { return function (req, res, next) { const now = Date.now(); const consumerId = req.consumerId; 

res.setHeader('Deprecation', toStructuredFieldDate(deprecationDate));
res.setHeader('Sunset', new Date(sunsetDate).toUTCString());
res.setHeader('Link', `&amp;lt;${migrationLink}&amp;gt;; rel="deprecation"`);

telemetry.recordHit('headers', consumerId);

if (killSwitch()) {
  return next(); // incident-response rollback: headers stay, no enforcement
}

const activeStage = stages
  .filter((s) =&amp;gt; now &amp;gt;= s.startsAt)
  .sort((a, b) =&amp;gt; b.startsAt - a.startsAt)[0];

if (!activeStage || !isEnforced(consumerId)) {
  return next();
}

switch (activeStage.mode) {
  case 'retired': {
    telemetry.recordOutcome('retired', consumerId, '410');
    return res.status(410).json({
      error: 'gone',
      message: 'This endpoint was retired. See the Link header for the migration guide.',
    });
  }
  case 'brownout': {
    const { rejectRatio, retryAfterSeconds } = activeStage.config;
    if (Math.random() &amp;lt; rejectRatio) {
      telemetry.recordOutcome('brownout', consumerId, '429');
      res.setHeader('Retry-After', String(retryAfterSeconds));
      return res.status(429).json({
        error: 'deprecated_brownout',
        message: `Scheduled brownout in effect per the published retirement schedule. See ${migrationLink}.`,
      });
    }
    telemetry.recordOutcome('brownout', consumerId, 'pass');
    return next();
  }
  case 'quota': {
    req.deprecationQuotaTag = activeStage.config.quotaKey;
    return next();
  }
  default:
    return next();
   }
 }; 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stages here are explicit, scheduled, published data rather than a continuous random function tied to elapsed time. Enforcement is per-consumer and gated by an allowlist so a rollout can ramp in stages instead of switching on for every consumer at the same time. A kill switch supports incident response, and every enforcement decision is recorded to telemetry so it can feed the decision loop below.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Telemetry as the backbone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;"Track usage per consumer" isn't a strategy on its own. The strategy is the decision loop that telemetry feeds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Identify every consumer still hitting the deprecated path, from API keys/auth identity, not just aggregate request counts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Segment by risk and traffic. A partner integration processing payments and a dormant internal script hitting the endpoint twice a day need different handling and different owners.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notify owners, not just via the changelog. The segmented list makes targeted outreach possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Measure migration after each stage, before deciding to advance. The gate for moving to the next stage should be a measured drop in remaining traffic plus confirmed outreach, not just a date on a calendar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enforce the next stage only for consumers who haven't moved. Scope enforcement to the remaining, already-notified cohort rather than applying it to everyone the moment a date arrives.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This loop is also what tells you when to pause: if a critical, contractually-protected partner is still on the old path with an active migration in progress, the gate holds them back from the next enforcement stage while the rest of the cohort proceeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Testing the strategy before shipping it&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before rolling out a staged retirement for a public or partner API, &lt;a href="https://geekyants.com/engineering/quality-assurance/api-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;test the full sequence&lt;/strong&gt;&lt;/a&gt; of headers, warnings, one brownout window, quota tightening, and retirement against a low-traffic internal endpoint with a small, known set of consumers. Compress the sequence into a shorter window than a real public deprecation would use.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What to Capture at Each Stage&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Request volume to the deprecated path, before and after each stage transition&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Number of distinct consumers still active, and how that count changes after outreach vs. after the first brownout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry behavior during the brownout window, including whether clients back off per Retry-After or retry at a rate that begins to resemble an attack&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Support/Slack messages generated per stage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time from "notified" to "migrated" per consumer, segmented by whether they moved after notice alone or only after a brownout&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test report should show the share of consumers who migrated after headers and direct outreach, the share who required a brownout window, the number of integrations requiring one-on-one follow-up, and any unexpected brownout behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What actually breaks&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Staged retirements do not fail cleanly, so teams need to plan for the ways they can fail.&lt;/p&gt;

&lt;p&gt;A brownout window can trigger a retry loop that amplifies load instead of shedding it if clients do not respect Retry-After. That is why the header has to be present on every throttled response, not just the first.&lt;/p&gt;

&lt;p&gt;A batch job can fail without drawing attention if it does not check for 429 at all. The problem may surface when someone notices missing data downstream.&lt;/p&gt;

&lt;p&gt;A dormant integration with no listed owner can remain invisible until an enforcement stage breaks it. That is why the consumer registry needs an ownership field checked before enforcement begins, not after.&lt;/p&gt;

&lt;p&gt;None of these are arguments against staged enforcement. They are arguments for building the telemetry and allowlist machinery described above before turning any stage on. If something behaves in an unexpected way, it should show up in a dashboard instead of a support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why this matters now&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;API environments now include &lt;a href="https://geekyants.com/engineering/backend/enterprise-backend-development" rel="noopener noreferrer"&gt;&lt;strong&gt;internal microservice contracts&lt;/strong&gt;&lt;/a&gt;, partner integrations, and a growing layer of &lt;a href="https://geekyants.com/blog/ai-agents-the-next-frontier-in-intelligent-automation" rel="noopener noreferrer"&gt;&lt;strong&gt;AI agents&lt;/strong&gt;&lt;/a&gt; and tools that can call APIs without a person reading a changelog. A deprecation strategy that depends on a person noticing an email doesn't scale to that world. One built into the protocol through headers, a published schedule, and telemetry-gated enforcement does.&lt;/p&gt;

&lt;p&gt;Sunset timelines don't fail because nobody announced them. They fail because, for most of the window, nothing about using the deprecated path changed. The fix isn't writing a better warning, and it isn't making things worse without notice until the deadline. It's a retirement plan the API keeps, announced one stage at a time. By the retirement date, consumers should know it is coming because they have seen it on the schedule, not because they experienced the change without warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Build APIs for Change, Not Just Launch&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;API reliability extends beyond keeping an endpoint available. Teams also need a plan for how APIs evolve, how consumers migrate, and how older versions are retired without creating avoidable production failures. Staged retirement, telemetry, and controlled enforcement make deprecation part of the API lifecycle rather than a last-minute cutoff. Explore how GeekyAnts approaches scalable systems and API architecture through its &lt;a href="https://geekyants.com/engineering/backend" rel="noopener noreferrer"&gt;&lt;strong&gt;Backend Engineering&lt;/strong&gt;&lt;/a&gt; capabilities.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>From Manual Testing to AI-Assisted Automation with Playwright Agents</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:01:38 +0000</pubDate>
      <link>https://dev.to/geekyants/from-manual-testing-to-ai-assisted-automation-with-playwright-agents-48n</link>
      <guid>https://dev.to/geekyants/from-manual-testing-to-ai-assisted-automation-with-playwright-agents-48n</guid>
      <description>&lt;p&gt;This blog discusses the value of Playwright Agents in automating workflows. It provides a detailed description of setting up the system, as well as a breakdown of the Playwright Agent’s automation process.&lt;/p&gt;

&lt;p&gt;For years, &lt;a href="https://geekyants.com/en-in/engineering/quality-assurance/qa-automation-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;automation engineers&lt;/strong&gt;&lt;/a&gt; have followed a familiar rhythm. Requirements come in, test cases are written, scripts are automated, locators break, scripts fail, and debugging begins. Fix, re-run, repeat.&lt;/p&gt;

&lt;p&gt;This cycle hasn’t changed much, even though frameworks have evolved from Selenium to Cypress to modern tools like Playwright.&lt;/p&gt;

&lt;p&gt;What if your automation framework didn’t just execute tests — what if it planned them, wrote them, ran them, and even fixed them when they broke? That’s exactly what &lt;a href="https://geekyants.com/en-in/blog/how-ai-ml-are-transforming-quality-assurance-in-software-testing-with-playwright-examples" rel="noopener noreferrer"&gt;&lt;strong&gt;Playwright Test Agents&lt;/strong&gt;&lt;/a&gt; bring to the table.&lt;/p&gt;

&lt;p&gt;Playwright introduced these &lt;a href="https://geekyants.com/en-in/ai/ai-agent-development-services" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-powered agents&lt;/strong&gt;&lt;/a&gt; in version 1.56 to automate key parts of the testing lifecycle — planning, generating, and healing tests — using an agentic loop that interacts with your live application.&lt;/p&gt;

&lt;p&gt;In this blog, we’ll explore what Playwright Agents are, how to set them up, how to use seed tests and prompts, what files they generate (like in your screenshot), and how each agent works in the development lifecycle. We’ll close with practical tips on prompt design and real differences versus generic &lt;a href="https://geekyants.com/en-in/blog/top-8-ai-coding-tools-for-developers-in-the-usa-2025-edition" rel="noopener noreferrer"&gt;&lt;strong&gt;AI coding tools&lt;/strong&gt;&lt;/a&gt; like Cursor or ChatGPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Evolution of Automation with Playwright&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Playwright became popular by addressing common automation challenges like flaky tests and synchronization issues. With features like automatic waiting, semantic locators such as &lt;strong&gt;getByRole&lt;/strong&gt;, and built-in tracing, it reduced the effort required to stabilize tests. This allowed QA engineers to focus more on test coverage rather than debugging framework issues. However, even with these improvements, designing and maintaining test scripts still remained a manual effort.&lt;/p&gt;

&lt;p&gt;We still had to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Translate requirements into scenarios&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Convert scenarios into code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refactor when UI changes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix broken locators&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Playwright Agents aim to assist in exactly those areas.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Introducing Playwright Test Agents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Playwright Test Agents are &lt;a href="https://geekyants.com/en-in/blog/revolutionizing-business-process-automation-with-ai-agents" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-assisted automation workflows&lt;/strong&gt;&lt;/a&gt; embedded directly into your Playwright project, designed to help you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Explore an application and produce a test plan&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transform that plan into executable Playwright test code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run tests and automatically repair failures&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are three core agents:&lt;/p&gt;

&lt;p&gt;The Planner Agent takes natural language input and converts it into structured test scenarios. It uses the seed test as context to explore the application, understand user flows, and identify possible edge cases. The output is a detailed test plan with steps and expected outcomes, similar to how a &lt;a href="https://geekyants.com/en-in/service/hire-quality-assurance-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;QA engineer&lt;/strong&gt;&lt;/a&gt; would design test cases.&lt;/p&gt;

&lt;p&gt;The Generator Agent takes these structured scenarios and converts them into executable Playwright scripts. While generating code, it interacts with the live application to validate selectors, identify stable locators, and ensure assertions reflect actual UI behavior. It can also follow architectural patterns like Page Object Model, producing manageable and scalable test code.&lt;/p&gt;

&lt;p&gt;The Healer Agent focuses on maintaining test stability. When a test fails, it replays the scenario, inspects the DOM, and identifies what caused the failure. It then attempts to fix the issue by updating selectors, adjusting waits, or modifying interaction logic, reducing the manual effort required for test maintenance.&lt;/p&gt;

&lt;p&gt;These agents can be invoked independently or chained together in a complete “agentic loop”:&lt;/p&gt;

&lt;p&gt;Planner → Generator → Healer&lt;/p&gt;

&lt;p&gt;This turns a natural language description of test requirements into a stable test suite with minimal manual coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Project Setup — Step by Step (Beginner Friendly)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you already know basic &lt;a href="https://geekyants.com/en-in/blog/automation-testing-with-playwright-using-javascript" rel="noopener noreferrer"&gt;&lt;strong&gt;Playwright automation&lt;/strong&gt;&lt;/a&gt;, this should feel like an extension of that knowledge. If not, stick with it. By the end, you will understand how these agents help even if you’re new to automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a Playwright Project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start by creating a new directory and initializing a Node project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir playwright-agent-demo
cd playwright-agent-demo
npm init -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -D @playwright/test
npx playwright install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Now install Playwright:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You now have a basic Playwright project.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Initialize Playwright Agents&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To add agent definitions to your project, run:&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%2Fsibd4eate3a0twif9ks4.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%2Fsibd4eate3a0twif9ks4.png" alt=" " width="800" height="58"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This command generates agent files in your project — which you’ll recognize in the screenshot below:&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%2F98c985iup3l77ijffh1o.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%2F98c985iup3l77ijffh1o.png" alt=" " width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A folder named &lt;strong&gt;.github/agents&lt;/strong&gt; contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;playwright-test-planner.agent.md&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;playwright-test-generator.agent.md&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;playwright-test-healer.agent.md&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the agent definitions that your AI tool (like Claude Code, VS Code Copilot, or OpenCode) uses to understand how to plan, generate, and heal tests.&lt;/p&gt;

&lt;p&gt;Under the root folder, you also see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;specs/&lt;/strong&gt; – for Markdown plans&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tests/&lt;/strong&gt; – for generated Playwright test files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;seed.spec.ts&lt;/strong&gt; – a seed test that bootstraps the environment&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This exact file structure is aligned with Playwright’s agent conventions: &lt;strong&gt;.github/agents&lt;/strong&gt;, &lt;strong&gt;specs/&lt;/strong&gt;, and &lt;strong&gt;tests/&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A seed test is essential because it provides a starting context that the planner uses to understand where to begin exploration, including any setup required (like logging in or navigating to a landing page).&lt;/p&gt;

&lt;p&gt;Create &lt;strong&gt;tests/seed.spec.ts&lt;/strong&gt;:&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%2Fuihn3j2fcppoz9q44n3m.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%2Fuihn3j2fcppoz9q44n3m.png" alt=" " width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, add dummy seed data to a JSON file (optional but recommended):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;testdata/seed.json&lt;/strong&gt;&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%2Fll3eklghqae6uj3i91y0.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%2Fll3eklghqae6uj3i91y0.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The seed test and seed data help the Planner understand context and scenarios, which makes its output far more relevant and accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How the Planner Agent Works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Planner Agent is like a QA analyst powered by &lt;a href="https://geekyants.com/en-in/ai" rel="noopener noreferrer"&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/a&gt;. Rather than immediately writing code, it first produces a structured Markdown test plan that describes required test scenarios, user flows, steps, expected outcomes, and test data.&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%2F2x6mpnj95b9wpc034ile.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%2F2x6mpnj95b9wpc034ile.png" alt=" " width="494" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can review this file before moving to code generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Generator Agent: Turning Plans into Code&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once you have a test plan, it’s time to generate actual automation scripts.&lt;/p&gt;

&lt;p&gt;Switch your &lt;a href="https://geekyants.com/en-in/artificial-intelligence-consulting" rel="noopener noreferrer"&gt;&lt;strong&gt;AI assistant&lt;/strong&gt;&lt;/a&gt; to Generator mode and provide a prompt such as:&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%2F21ucw7ak9ip2cxsgdkjn.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%2F21ucw7ak9ip2cxsgdkjn.png" alt=" " width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Generator reads the Markdown plan, actively interacts with the browser to verify selectors and assertions, and produces test scripts under the &lt;strong&gt;tests/&lt;/strong&gt; directory. Similar to this:&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%2Fup1985c5q5fntcz8h1f7.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%2Fup1985c5q5fntcz8h1f7.png" alt=" " width="799" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, provide a prompt like:&lt;/p&gt;

&lt;p&gt;“Create a test plan for login functionality with valid and invalid user scenarios using the seed test context.”&lt;/p&gt;

&lt;p&gt;The Planner will explore your live application (through the seed test) and generate a Markdown file under s*&lt;em&gt;pecs/&lt;/em&gt;* such as:&lt;/p&gt;

&lt;p&gt;specs/login-plan.md&lt;/p&gt;

&lt;p&gt;This file contains detailed, human-readable test plans, not code, but instructions for how you want the generator to build tests.&lt;/p&gt;

&lt;p&gt;This step mirrors the typical QA process of writing test case documentation, except that the agent generates it automatically.&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%2Fvm7wt7k8ru3kijrj7mi8.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%2Fvm7wt7k8ru3kijrj7mi8.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each test should mirror a scenario from the plan.&lt;/p&gt;

&lt;p&gt;Because the Generator interacts directly with the live app and evaluates selectors as it writes code, the tests it generates are often more stable and accurate than typical prompt-only AI output.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Healer Agent: Using AI to Fix Failing Tests&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Inevitably, tests fail. It might be due to UI changes, such as an updated locator or changed button label.&lt;/p&gt;

&lt;p&gt;Traditionally, you would open your editor, inspect the DOM, update selectors, and re-run tests. With Playwright’s Healer Agent, this can be assisted by AI.&lt;/p&gt;

&lt;p&gt;Invoke the healer with a prompt like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Run and fix the failing test tests/amazon-search-add-to-cart-edge.spec.spec.ts.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The healer will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Replay the failing test in debug mode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inspect the DOM to find equivalent elements or flows&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Propose updates to locators or waits&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re-run until the test passes, or decide that the test really reflects a broken feature.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This reduces repeated manual debugging cycles, especially for tests that only break due to minor UI refactors.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;From Prompt to Execution: Inside the Agent Workflow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;While using Playwright Test Agents feels simple from a user perspective, there is significant processing happening in the background.&lt;/p&gt;

&lt;p&gt;The agents operate through an agentic loop where they can read project files, execute tests, interact with the browser, and inspect the live DOM. For example, the Planner uses the seed test to explore the application and understand flows, the Generator validates selectors in real time while generating scripts, and the Healer replays failing tests to identify and fix issues.&lt;/p&gt;

&lt;p&gt;In the foreground, this complexity is abstracted into simple inputs and outputs. Users provide prompts and receive structured test plans, executable test scripts, or suggested fixes without directly interacting with the underlying processes. This separation is what makes Playwright Agents both powerful and easy to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Structuring Tests with Page Object Model&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the biggest benefits of designer prompts is instructing the generator to produce maintainable code, and that starts with architecture.&lt;/p&gt;

&lt;p&gt;If you prompt:&lt;/p&gt;

&lt;p&gt;“Use Page Object Model and store locators in separate page files.”&lt;/p&gt;

&lt;p&gt;The generator will output something like:&lt;/p&gt;

&lt;p&gt;pages/login.page.ts&lt;/p&gt;

&lt;p&gt;tests/login/login.spec.ts&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;login.page.ts&lt;/strong&gt; contains locator definitions and reusable page actions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;login.spec.ts&lt;/strong&gt; uses the page object and seed data for test logic&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This results in a clean, maintainable automation framework that scales well.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Agentic Loop: From Plan to Stable Tests&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When you use all three agents together, you get:&lt;/p&gt;

&lt;p&gt;Seed Test + Prompt&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Planner → Create Markdown Plan&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Generator → Create Tests&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Healer → Fix Failures&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Stable Automation Suite&lt;/p&gt;

&lt;p&gt;This mirrors a full human automation lifecycle, except now it is assisted by AI and deeply integrated with Playwright’s tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Playwright Agents Differ from Generic AI Tools&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Regular AI Code Generation&lt;/th&gt;
&lt;th&gt;Playwright Agents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generates code based on prompts&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs tests&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixes failing tests autonomously&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Understands live DOM while generating code&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrated into the Playwright ecosystem&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It’s easy to confuse Playwright Agents with other AI coding tools, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cursor AI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ChatGPT code generation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generic AI assistants&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is a fundamental difference:&lt;/p&gt;

&lt;p&gt;Playwright Agents integrate with MCP (Model Context Protocol) and interact with your application and tests as part of the lifecycle. This makes them far more context-aware and useful than simple prompt-to-code generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Best Practices for Using Playwright Test Agents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here are some practical tips based on real usage trends:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provide Good Context&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Always include a clear seed test&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use structured seed data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reference environment details&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Write Clear Prompts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make sure your prompts include architecture preferences, test data references, and expected outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review Generated Tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI can generate great boilerplate, but human review is still important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrate into CI Carefully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treat healed and generated tests as drafts until fully reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Playwright Agents, Planner, Generator, and Healer bring AI directly into the automation lifecycle. They:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Plan test scenarios from natural language&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generate well-structured automation code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check and repair failing tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Help QA teams move faster with less manual overhead&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any QA engineer with basic Playwright knowledge, these agents unlock productivity leaps, from planning without code to generating and healing tests with AI.&lt;/p&gt;

&lt;p&gt;If you want to experiment with this in your own project, run the agent setup, build a seed test, and start with simple prompts. You will be amazed at how much of the automation lifecycle can now be AI-assisted.&lt;/p&gt;

</description>
      <category>technology</category>
      <category>ai</category>
      <category>testing</category>
      <category>qualityassurance</category>
    </item>
    <item>
      <title>From Prompting to Process: What Changed When Flutter Shipped Agent Skills</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:44:01 +0000</pubDate>
      <link>https://dev.to/geekyants/from-prompting-to-process-what-changed-when-flutter-shipped-agent-skills-38p9</link>
      <guid>https://dev.to/geekyants/from-prompting-to-process-what-changed-when-flutter-shipped-agent-skills-38p9</guid>
      <description>&lt;p&gt;Google is starting to ship Flutter's engineering workflows as machine-readable guidance for AI agents. It may look like another AI feature, but it hints at a much bigger shift in how teams build with Flutter.&lt;/p&gt;

&lt;p&gt;The Consistency Problem&lt;/p&gt;

&lt;p&gt;Over the past few years, AI coding tools like Cursor, Claude Code, Copilot, and OpenCode have become part of many developers' daily workflows. They produce code, explain unfamiliar APIs, write tests, and navigate large codebases with impressive accuracy.&lt;/p&gt;

&lt;p&gt;But if you have been using them on a reasonably large project, you have probably noticed something.&lt;/p&gt;

&lt;p&gt;They are not very consistent.&lt;/p&gt;

&lt;p&gt;Ask an AI to implement the same feature in two different sessions, and there's a good chance you will get two different approaches. Switch models, and the implementation changes again. Sometimes it follows the latest framework recommendations. Other times, it relies on outdated patterns.&lt;/p&gt;

&lt;p&gt;Most teams respond the same way: write better prompts, add repository rules, or create a custom skill.md files to steer the agent toward the right decisions.&lt;/p&gt;

&lt;p&gt;We took the same approach by documenting our architecture, coding conventions, review expectations, and engineering practices as custom Skills. They made the agent noticeably more consistent.&lt;/p&gt;

&lt;p&gt;So when Flutter announced official Agent Skills, my first reaction was not: "How do we use them?" It was: "If we already have our own Skills, what problem are Flutter's official Skills actually solving?"&lt;/p&gt;

&lt;p&gt;That question turned into a couple of experiments.&lt;/p&gt;

&lt;p&gt;Three Layers of AI-Assisted Development&lt;/p&gt;

&lt;p&gt;Looking back, Flutter's recent AI investments weren't isolated features. They were building on each other.&lt;/p&gt;

&lt;p&gt;First came Rules, giving teams a way to define project-specific conventions and preferences.&lt;/p&gt;

&lt;p&gt;Then came Model Context Protocol (MCP), allowing AI agents to inspect, debug, and interact with running Flutter applications instead of reasoning purely from static code.&lt;/p&gt;

&lt;p&gt;And then came Agent Skills.&lt;/p&gt;

&lt;p&gt;If Rules tell an agent how your team works, and MCP tells it what's happening inside your application, Agent Skills answer a different question: how does Flutter itself recommend solving this problem?&lt;/p&gt;

&lt;p&gt;That is the important shift.&lt;/p&gt;

&lt;p&gt;Flutter is now versioning its engineering workflows alongside the framework itself. Instead of relying entirely on what an AI model happened to learn during training, agents can follow workflows maintained by the Flutter team.&lt;/p&gt;

&lt;p&gt;Today, those workflows cover areas like:&lt;/p&gt;

&lt;p&gt;Localization&lt;br&gt;
Responsive layouts&lt;br&gt;
Routing&lt;br&gt;
Widget and unit testing&lt;br&gt;
Static analysis&lt;br&gt;
JSON serialization&lt;br&gt;
Platform integration&lt;br&gt;
Architecture best practices&lt;/p&gt;

&lt;p&gt;In other words, Flutter is shipping its engineering knowledge as structured workflows.&lt;/p&gt;

&lt;p&gt;That naturally led to the next question: does this actually change how an AI agent behaves?&lt;/p&gt;

&lt;p&gt;I ran two experiments to find out.&lt;/p&gt;

&lt;p&gt;First Experiment: Declarative Routing&lt;/p&gt;

&lt;p&gt;It is one of those features where there is not just one thing to do. Depending on the prompt, an AI could jump straight into writing routes, miss platform-specific configuration, skip deep linking altogether, or recommend an approach based on what it learned during training rather than Flutter's latest guidance.&lt;/p&gt;

&lt;p&gt;So I kept the prompt intentionally simple:&lt;/p&gt;

&lt;p&gt;"Set up declarative routing for this Flutter application."&lt;/p&gt;

&lt;p&gt;I wanted to see how the agent would approach the problem before I told it how to solve it.&lt;/p&gt;

&lt;p&gt;The first thing it did caught my attention.&lt;/p&gt;

&lt;p&gt;Before generating an implementation plan, it explicitly selected the flutter-setup-declarative-routing skill.&lt;/p&gt;

&lt;p&gt;Show Image&lt;/p&gt;

&lt;p&gt;From that point on, it was not about figuring out a solution; it was following Flutter's own workflow.&lt;/p&gt;

&lt;p&gt;That was the interesting part.&lt;/p&gt;

&lt;p&gt;Without Agent Skills, the implementation depends on the model's reasoning and whatever Flutter knowledge it has internalized. With Agent Skills, the framework itself becomes the starting point.&lt;/p&gt;

&lt;p&gt;What happens when Flutter's Skills and our own Skills are both available?&lt;/p&gt;

&lt;p&gt;I tested this with a login screen.&lt;/p&gt;

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

&lt;p&gt;Flutter's localization workflow&lt;br&gt;
Our project conventions for authentication, repositories, dependency injection, and state management&lt;/p&gt;

&lt;p&gt;I kept the prompt simple and let the agent decide how to approach it -&lt;/p&gt;

&lt;p&gt;"Implement a login screen for this application. Follow the existing project architecture and add localization for all user-facing strings.&lt;/p&gt;

&lt;p&gt;It picked the right source every time.&lt;/p&gt;

&lt;p&gt;For localization, it used Flutter's official Skill. For everything related to our application architecture, it followed our custom Skills. I did not have to tell it which one to use or write a carefully engineered prompt.&lt;/p&gt;

&lt;p&gt;The two sets of Skills worked together naturally. Flutter handled the framework guidance, while our repository continued to define how our application was built.&lt;/p&gt;

&lt;p&gt;Show Image&lt;/p&gt;

&lt;p&gt;Why This Matters: A Floor, Not a Finish Line&lt;/p&gt;

&lt;p&gt;So, what actually changed?&lt;/p&gt;

&lt;p&gt;Flutter is taking ownership of its engineering expertise.&lt;/p&gt;

&lt;p&gt;Engineering teams no longer need to teach AI how Flutter expects routing, localization, responsive layouts, or testing to be implemented. Flutter now ships that knowledge itself.&lt;/p&gt;

&lt;p&gt;That removes a significant amount of duplication. Instead of every team maintaining its own version of Flutter best practices inside prompts or custom Skills, the official workflows become the source of truth.&lt;/p&gt;

&lt;p&gt;Even better, those workflows evolve alongside Flutter. As recommendations change, the official Skills change too, and every compatible AI agent benefits automatically.&lt;/p&gt;

&lt;p&gt;Of course, that's only half the story.&lt;/p&gt;

&lt;p&gt;Flutter's Agent Skills provide a strong foundation, but they do not replace your organization's architecture, coding standards, or business-specific workflows. Those remain your responsibility, and that is exactly how it should be.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;Looking back, Flutter's recent AI features fit together surprisingly well.&lt;/p&gt;

&lt;p&gt;Rules capture how your team works. MCP gives agents runtime context. Agent Skills teach them how Flutter itself expects problems to be solved. Custom Skills layer on everything unique to your organization.&lt;/p&gt;

&lt;p&gt;Together, they reduce the amount of engineering knowledge an AI has to infer. That is the real significance of Flutter's recent AI investments. Engineering knowledge is becoming explicit, versioned, reusable, and maintained by the people best positioned to own it.&lt;/p&gt;

&lt;p&gt;Today's skill library covers foundational workflows, but it already hints at what's possible. Imagine Skills for performance profiling, DevTools workflows, accessibility audits, plugin development, migrations, or advanced rendering patterns. Every new skill moves another piece of framework knowledge out of documentation and into a reusable workflow.&lt;/p&gt;

&lt;p&gt;The answer to how much of Flutter an agent should really guess keeps shrinking with every release.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ai</category>
      <category>promptengineering</category>
      <category>agents</category>
    </item>
    <item>
      <title>Feature Flags as Technical Debt: The Cleanup Nobody Schedules</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:46:47 +0000</pubDate>
      <link>https://dev.to/geekyants/feature-flags-as-technical-debt-the-cleanup-nobody-schedules-1j9o</link>
      <guid>https://dev.to/geekyants/feature-flags-as-technical-debt-the-cleanup-nobody-schedules-1j9o</guid>
      <description>&lt;p&gt;Feature flags are one of the cheapest tools in engineering to adopt and one of the most expensive to leave unmanaged. Teams use them for gradual rollouts, A/B tests, kill switches, and gating unfinished work, but few teams have a matching process for removing them once they have served their purpose. As a result, flags that were meant to be temporary become permanent, adding unnecessary complexity to the codebase.&lt;/p&gt;

&lt;p&gt;This article explains why flag cleanup gets skipped and why it creates a real engineering cost. It also walks through a staleness-detection implementation, a flag-removal exercise, and a practical checklist for managing flags across their full lifecycle, from creation to removal.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Flags Are Easy to Add and Hard to Remove&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Adding a flag is a small, fast PR. Wrapping a block of code in a conditional and wiring it to a config value takes minutes, and it ships in the same PR as the feature it gates — there's no separate approval step, no extra review, no reason for anyone to push back. Removing a flag involves a different kind of work. You have to find every place it is checked, including references in logging, analytics, and monitoring code, confirm which branch is now permanently "on" or "off," delete the dead branch, update or delete the tests that covered it, and verify that nothing downstream depends on the old behavior. This multi-step effort competes with new feature work for the same sprint capacity, so cleanup often gets pushed to a later sprint until the flag becomes a permanent part of the codebase.&lt;/p&gt;

&lt;p&gt;There's also a confidence problem. Once a flag has been live for months, the person who added it may have moved teams, changed roles, or forgotten why it was added. Removing the conditional can feel risky when its dependencies are unclear, so teams may leave it in place. That decision can turn a two-week release flag into a permanent part of the codebase.&lt;/p&gt;

&lt;p&gt;Underneath both of these is a structural gap: a flag rarely comes with a ticket, an expiry date, or a named owner responsible for its removal. By default, creating a flag does not create a corresponding task to revisit it later. Without documented future work, the flag can remain in place. Meanwhile, teams add new flags every sprint while removing few of the old ones, so the backlog grows and cleanup becomes more complex as additional flags enter the same code paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Flag Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The diagram below lays out the full path a flag should take, from creation to retirement. The critical fork sits in the middle of the diagram: once a flag reaches "stable at 100%," it either gets caught by an automated staleness check and routed to a removal PR, or it gets ignored and drifts into permanent technical debt. Most flags fail at exactly this point — not because removal is technically difficult, but because nothing in the default engineering workflow forces the question to be asked. Without an automated trigger, a flag can sit at "stable" for years with nobody ever explicitly deciding to leave it that way; it simply never comes up.&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%2F8qokj4l5h2e5loqsajio.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%2F8qokj4l5h2e5loqsajio.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 1: The feature flag lifecycle, from creation through rollout to either scheduled removal or stale limbo.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why This Is a Real Cost, Not Just Clutter&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Each independent binary flag can double the number of possible runtime states. A function gated by three independent flags can have up to eight possible states, but teams may have designed or &lt;a href="https://geekyants.com/engineering/quality-assurance/functional-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;tested&lt;/strong&gt;&lt;/a&gt; only a subset of those combinations. The remaining states can introduce interactions that were not considered during development or testing.&lt;/p&gt;

&lt;p&gt;Stale flags can create bugs through untested interactions. Two flags that are each considered "basically always on" can still interact in a combination the team did not anticipate. If the team assumes that only one meaningful state is live, that interaction may not be covered during testing or code review and can surface only under production traffic.&lt;/p&gt;

&lt;p&gt;New engineers may avoid code affected by unfamiliar flags. When it is unclear which flags are critical and which can be removed, team members may work around that code rather than risk breaking an unknown dependency. This can slow down simple changes and introduce workarounds that add further complexity.&lt;/p&gt;

&lt;p&gt;The number of dependencies can grow the longer a flag remains in place. Flag checks can extend into related systems, including analytics events tied to flag state, log lines that reference the flag, monitoring dashboards built around it, and configurations in other services. As these dependencies accumulate, removing the flag can require changes across several parts of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Not All Flags Are the Same&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A major reason cleanup gets mishandled is that teams treat every flag the same, even though the appropriate lifespan depends on why the flag exists. Classifying a flag at creation establishes its expected lifespan and removal requirements before the original context is lost.&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%2Foa6ishiuwda41of87lzk.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%2Foa6ishiuwda41of87lzk.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;release flag&lt;/strong&gt; usually lasts from a few days to a few weeks and is used to gate an in-progress feature during development and rollout. Its cleanup urgency is high, so it should be removed once the feature has reached 100% rollout and the release has stabilized. An &lt;strong&gt;experiment flag&lt;/strong&gt; typically remains active only for the duration of a test, such as an A/B experiment or a gradual, data-driven rollout. Its cleanup urgency is also high, and it should be removed as soon as the experiment concludes, regardless of the outcome. An &lt;strong&gt;ops or kill-switch flag&lt;/strong&gt;, by contrast, is designed to remain in place indefinitely. It acts as a manual override for risky dependencies or emergency controls, so it does not need immediate removal. Instead, it should be reviewed periodically to confirm that it is still necessary and working as intended.&lt;/p&gt;

&lt;p&gt;This highlights a common cleanup problem: a release or experiment flag with a short intended lifespan can receive the same caution as an ops kill switch designed to remain in place. That mismatch can turn a two-week flag into a permanent one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Building the Staleness Detector&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The checklist below recommends automating staleness detection, so it helps to show what that implementation looks like. The following example presents the core logic, simplified for readability. In practice, it receives data from the flag provider in use, such as LaunchDarkly, Unleash, or a homegrown configuration table, with each flag providing a rollout percentage, type, and the length of time its state has remained unchanged.&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%2Fz0d8rbrnjwij86ayseaw.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%2Fz0d8rbrnjwij86ayseaw.png" alt=" " width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two design choices here matter more than the code itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thresholds are per-type, not global.&lt;/strong&gt; A single "flag unchanged for 90 days" rule either fires constantly on ops flags that are correctly untouched, or lets release flags rot for far too long. Splitting the threshold by type — pulled directly from the classification the team should already be doing at creation — makes the report something people trust instead of something they learn to ignore.&lt;/p&gt;

&lt;p&gt;The report identifies UNASSIGNED owners. An unowned stale flag has no person or team responsible for acting on the report, which can leave it in the codebase without a clear path to removal. Surfacing that ownership gap instead of defaulting to "team lead" makes responsibility for cleanup visible.&lt;/p&gt;

&lt;p&gt;Wiring this into a weekly Slack post or a lightweight internal dashboard, including a spreadsheet as a starting point, turns staleness into something the team reviews on a fixed cadence rather than discovers during an unrelated investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Removing a Stale Flag: A Worked Example&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Detection is only half the problem. Deleting a flag safely requires a defined procedure because missed dependencies can create production issues. The sequence below shows how to remove a release flag that has remained at 100% for several weeks, using a checkout-discount flag as an example.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm the resolved state, not just the current one.&lt;/strong&gt; Check the flag's rollout history, not just its current value — a flag sitting at 100% today that was flipped back to 0% twice in the last month is not actually stable, regardless of what the staleness report says this week.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Search for every reference, not just the obvious one.&lt;/strong&gt; A project-wide search for the flag's key (checkout_discount_v2) can reveal references beyond the if branch in the checkout service, including a log line that prints the flag's value, an analytics event property, and a conditional in a monitoring dashboard's alert query. All of these references need to be accounted for so the cleanup does not leave dead references behind.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delete the dead branch, not just the flag check.&lt;/strong&gt; Before removal, the code checks the flag and branches between apply_discount_v2(cart) and apply_discount_legacy(cart). After removal, it calls apply_discount_v2(cart) directly, with both the flag check and the unused branch removed. Leaving both branches as dead code "just in case" preserves unnecessary code outside the flag inventory. If apply_discount_legacy has no other callers, remove it as well.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remove the tests for the dead branch, not just add tests for the surviving one.&lt;/strong&gt; The legacy-path test coverage is now testing code that no longer exists in any reachable state; leaving it in the suite either silently rots (mocking a function that's been deleted) or keeps a maintenance burden alive for behavior nobody can trigger anymore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ship it as its own PR, reviewed as a deletion.&lt;/strong&gt; Bundling flag removal into an unrelated feature PR can reduce the attention given to the search results from step 2. A standalone "remove checkout_discount_v2" PR keeps the review focused on deletion. Any addition in that diff warrants additional review. This procedure is often undocumented, which can make cleanup PRs feel riskier than they are.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When Cleanup Doesn't Happen: A Technical Walkthrough
&lt;/h2&gt;

&lt;p&gt;The scenario below illustrates a failure pattern common enough that most teams running flags at any scale will recognize a version of it, even if the specifics differ. A team adds checkout_discount_v2 to gate a new checkout flow. The rollout succeeds within two weeks and reaches 100% — by every functional measure, the flag has done its job. But it stays in the code for over a year, because removal was never written into any ticket and nobody was assigned to come back to it.&lt;/p&gt;

&lt;p&gt;Months later, a second flag, checkout_pricing_experiment, is added to the same checkout path for an unrelated pricing test. It runs after the discount flag: it takes whatever total the discount logic produced and applies an experimental pricing adjustment on top. Individually, each flag was tested and behaved correctly. However, apply_experimental_pricing was written and reviewed under the assumption that total came from apply_discount_legacy. By that point, checkout_discount_v2 had remained at 100% long enough that engineers working on the checkout path no longer considered it a meaningful variable, even though it remained in the code and continued to execute.&lt;/p&gt;

&lt;p&gt;apply_discount_v2 returned a total that had already been floored to two decimal places; apply_discount_legacy had not. apply_experimental_pricing applied a percentage multiplier and then rounded. This worked with apply_discount_legacy's unrounded output but could produce an off-by-one-cent total with apply_discount_v2's pre-rounded output for a narrow set of cart values. This type of bug can escape testing when no test covers the combination of two flags operating on the same code path. The fix required a two-line change to apply rounding consistently in one place. The greater cost came from the bug reaching production and requiring someone to identify a cent-level discrepancy in reconciliation data and trace it through a code path that was not expected to contain two active flags. This is the pattern the checklist below is designed to prevent: the gradual accumulation of untracked complexity that can lead to production issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Build vs. Buy: Tooling Trade-offs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Teams generally use one of three approaches for flag management, and the right choice depends less on team size than on how comfortable the organization is with an external SaaS dependency in the request path.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;managed feature flag platform&lt;/strong&gt;, such as LaunchDarkly, provides rich targeting rules, built-in audit logs and change history, and features for identifying stale flags or monitoring usage with relatively little setup. The tradeoff is recurring cost, which can increase with seats or monthly active users. It also introduces another network dependency into the request path, and while the platform may identify stale flags, someone still needs to own and act on that cleanup.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;open-source, self-hosted platform&lt;/strong&gt;, such as Unleash, avoids per-seat licensing costs and gives teams greater control over data residency and customization. However, the engineering team becomes responsible for operating the flag service, including uptime, upgrades, maintenance, and scaling. These platforms may also provide fewer built-in insights than managed alternatives unless additional monitoring and reporting are configured.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;homegrown configuration table&lt;/strong&gt; keeps the setup simple because it does not require introducing a separate feature flag platform. Teams can query the data directly and build custom staleness reports, such as the automated cleanup check described above. The downside is that there is usually no dedicated interface or audit trail unless those capabilities are built intentionally. Targeting logic can also spread across the codebase over time if it is not kept centralized.&lt;/p&gt;

&lt;p&gt;The staleness detector shown earlier works with all three approaches because it requires only a list of flags with a rollout percentage and a last-modified timestamp. This keeps the implementation compatible with each approach. Managed platforms may expose this information through a report or webhook, while the other two approaches can use the script above or a similar implementation. The tooling decision affects how much the team needs to build, but each approach still requires a process for detecting stale flags and assigning cleanup work.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Practical Checklist for Managing Flag Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Each step below maps directly to a stage in the feature flag lifecycle. Together, they turn the lifecycle from a diagram into a process that teams can follow consistently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Classify at creation.&lt;/strong&gt; Every flag should be tagged as a release, experiment, or ops flag when it is created. This establishes the expected lifespan and removal requirements from the beginning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Assign an owner and expiry.&lt;/strong&gt; Each flag should have a named person or team responsible for it, along with a target removal date, even if that date is approximate. Without clear ownership or a deadline, a flag can remain in the codebase indefinitely without a defined cleanup path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Bake removal into the ticket.&lt;/strong&gt; Flag removal should be included in the original story's definition of done. This prevents cleanup from becoming a separate task that has to compete for priority later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Automate staleness detection.&lt;/strong&gt; Teams should run scheduled checks for flags that have remained at 0% or 100% rollout beyond their type-specific staleness threshold. This makes stale flags visible without relying on someone to remember them manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Maintain a flag inventory.&lt;/strong&gt; A central dashboard should track every live flag, including its type, owner, and age. This gives teams a quick answer to questions such as how many flags are active and which ones may need attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Establish a recurring review cadence.&lt;/strong&gt; Teams should review flag ownership and staleness data monthly or quarterly. Regular reviews help catch neglected flags before they accumulate into a larger cleanup backlog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Prioritize removal PRs.&lt;/strong&gt; Pull requests that remove obsolete flags should be treated as focused cleanup work. Because these changes primarily remove code rather than add new behavior, they can often have a narrower review scope while reducing unnecessary complexity in the codebase.&lt;/p&gt;

&lt;p&gt;A note on step 2: individual ownership can become outdated when the named owner changes teams or leaves the company. Tying ownership to a service or feature area rather than a specific person provides continuity when personnel change and reduces the risk of flags becoming orphaned.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Closing Thought&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Feature flags are useful, but flag creation represents only part of their lifecycle. A temporary flag without a removal plan can become a permanent addition to the codebase and increase its complexity over time.&lt;/p&gt;

&lt;p&gt;Closing that gap requires a per-type staleness threshold, a script that checks it on a schedule, and a removal procedure that treats deletion PRs as part of planned engineering work. This approach builds removal into the same process as creation, classifies flags by type when they are created, and uses automated staleness checks to identify flags that require attention. Together, these changes make flag removal part of the same engineering process as flag creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Make Feature Flag Cleanup Part of the Engineering Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Feature flags work best when removal is treated as part of the same &lt;a href="https://geekyants.com/blog/what-is-the-geekyants-agentic-development-life-cycle-how-adlc-changes-conventional-product-engineering" rel="noopener noreferrer"&gt;&lt;strong&gt;engineering lifecycle&lt;/strong&gt;&lt;/a&gt; as creation and rollout. Clear ownership, type-specific staleness checks, scheduled reviews, and focused removal PRs help teams prevent temporary controls from becoming permanent technical debt. For teams looking to strengthen these practices across deployment, automation, monitoring, and production operations, &lt;a href="https://geekyants.com/engineering/devops" rel="noopener noreferrer"&gt;&lt;strong&gt;GeekyAnts’ DevOps consulting services&lt;/strong&gt;&lt;/a&gt; provide support across the software delivery lifecycle.&lt;/p&gt;

</description>
      <category>techtalks</category>
      <category>devops</category>
      <category>softwaredevelopment</category>
      <category>featureflags</category>
    </item>
    <item>
      <title>The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 28 Sep 2026 06:46:46 +0000</pubDate>
      <link>https://dev.to/geekyants/the-bug-that-doesnt-show-up-in-code-review-why-your-flutter-web-app-reloads-on-safari-2elp</link>
      <guid>https://dev.to/geekyants/the-bug-that-doesnt-show-up-in-code-review-why-your-flutter-web-app-reloads-on-safari-2elp</guid>
      <description>&lt;p&gt;Demo example app*:* &lt;a href="https://github.com/manuindersekhon/flutter-image-memory-demo" rel="noopener noreferrer"&gt;&lt;strong&gt;github.com/manuindersekhon/flutter-image-memory-demo&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://geekyants.com/en-in/hire-ai-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-assisted teams&lt;/strong&gt;&lt;/a&gt; now ship correct code faster than ever. But this article is about a class of failure that is not in the code at all. It lives in the gap between what the code says and what the device actually does. And that gap is exactly where all the velocity we gained gets eaten back.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Bug Report That Made No Sense&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Some time back, one of our production apps had a leaderboard. A simple, scrollable list of 100+ players, each row with a name, a score, and a 50x50 profile picture. Nothing fancy.&lt;/p&gt;

&lt;p&gt;Then the bug reports started coming in.&lt;/p&gt;

&lt;p&gt;“The leaderboard page keeps refreshing on Safari.” “The app closed by itself on my iPhone while scrolling.” No error in the console. No crash log with a stack trace. Nothing reproducible on our development machines. The page would simply reload on Safari as if the user had pressed refresh, and on &lt;a href="https://geekyants.com/en-in/service/mobile-app/ios-app-development-services" rel="noopener noreferrer"&gt;&lt;strong&gt;iOS the app&lt;/strong&gt;&lt;/a&gt; would just disappear.&lt;/p&gt;

&lt;p&gt;Two different platforms, two different symptoms, and as it turned out, one single bug. One that no code review could have caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Code That Passed Review&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here is the code at the centre of it. This is roughly what our leaderboard row looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ListTile(
  leading: CircleAvatar(
    radius: 25,
    backgroundImage: NetworkImage(user.avatarUrl),
  ),
  title: Text(user.name),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a moment and review it. It compiles. It is an idiomatic Flutter, straight from the documentation. The analyzer is happy. It renders perfectly on the MacBook and in every demo. Whether a teammate wrote it or an &lt;a href="https://geekyants.com/en-in/ai/ai-agent-development-services" rel="noopener noreferrer"&gt;&lt;strong&gt;AI agent&lt;/strong&gt;&lt;/a&gt; generated it, any of us would have approved this diff.&lt;/p&gt;

&lt;p&gt;Nothing in the code is wrong. The defect only exists at the intersection of three layers that no code-level reviewer is looking at: &lt;em&gt;what the backend serves, what the image decoder does with it, and what the device does when memory runs out.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Layer One: What the URL Actually Returns&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Our users uploaded their profile pictures from their phones. A modern phone camera produces a 12-megapixel image, around 4032x3024 pixels, a few MB of JPEG. Our backend stored it exactly as uploaded, and the avatar URL served it exactly as stored.&lt;/p&gt;

&lt;p&gt;The UI contract says “50 pixel avatar”. The API contract says “whatever the user uploaded”. There is no type system, no lint rule, and no review checklist that connects these two, and that mismatch travels silently all the way to the user's device.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Layer Two: File Size Is Not Memory Size&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the part of image handling that is easy to forget. A JPEG is a compressed format, but a screen cannot draw compressed bytes. Before anything is rendered, the image is decoded into a raw bitmap: four bytes for every pixel.&lt;/p&gt;

&lt;p&gt;So that 3 MB JPEG from the user's phone becomes 4032 x 3024 x 4 bytes, which is about 46.5 MB of memory. For one avatar. In a 50-pixel circle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And here is the key Flutter behaviour&lt;/strong&gt;: unless you tell it otherwise, &lt;em&gt;Flutter decodes an image at its full intrinsic size,&lt;/em&gt; not at the size of the widget displaying it. The CircleAvatar's 50-pixel constraint never reaches the decoder. The full bitmap is decoded, kept, and scaled down on every frame.&lt;/p&gt;

&lt;p&gt;To verify this, we built a small demo app that recreates the leaderboard and prints what the engine actually holds for each image. This is not theory, this is the app reporting its own image cache:&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%2Fymn3g0szwt2qgkrxq6k3.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%2Fymn3g0szwt2qgkrxq6k3.png" alt="Chrome demo leaderboard showing Flutter image memory usage with full-size avatar decoding" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo leaderboard in Chrome. The bar at the bottom is the app reporting its own decoded image memory: avatar #1 is held at 4032x3024, 46.51 MB, for a 50px circle.&lt;/p&gt;

&lt;p&gt;Multiply that by a leaderboard. Scrolling through 100+ entries asks the engine for several gigabytes of decoded bitmaps. Flutter's built-in image cache has a 100 MB budget, but that budget only applies to images that are no longer on screen. Images that are currently visible, or kept alive by the layout, are held regardless of it. The cap we were all silently relying on was never going to save us.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Layer Three: Every Platform Has a Ceiling, and They Are All Different&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here is where the story splits by platform, and where “works on my machine” stops being a joke and becomes the actual mechanism of the bug.&lt;/p&gt;

&lt;p&gt;Chrome survives this abuse. It decodes images through a dedicated browser API into memory it can discard and re-decode at will. When we scrolled our naive leaderboard in Chrome, the tab stayed around a few hundred MB and nothing bad happened. This is exactly why the bug never appeared on our development machines.&lt;/p&gt;

&lt;p&gt;Safari has no such decode path. The decoded bitmaps land inside the tab's own web content process, and &lt;strong&gt;WebKit&lt;/strong&gt; enforces a hard memory budget on that process. On the same page, same scroll, Safari's process climbed to four times what Chrome used. Push further, and WebKit does exactly what its source code says it will do: &lt;em&gt;it kills the web content process and reloads the page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In our stress test, the process crossed 7 GB and was terminated, and the page came back fresh, exactly the “random refresh” our users reported.&lt;/strong&gt; Safari even tells the user politely: “This webpage was reloaded because it was using significant memory.”&lt;/p&gt;

&lt;p&gt;There is also a subtler symptom on iPads and iPhones. Before killing the page, WebKit fights back by purging decoded images. We watched the iOS Safari process balloon, get purged, and keep running with the leaderboard text intact but every avatar blank:&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%2Fugfh70yb1fwuajb1i3d0.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%2Fugfh70yb1fwuajb1i3d0.png" alt="iOS Safari leaderboard after WebKit memory purge causes profile avatars to disappear" width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;iOS Safari after WebKit's memory purge: the page survives, but the avatars are silently gone.&lt;/p&gt;

&lt;p&gt;And the native iOS app? Same bug, different executioner. iOS enforces a per-app memory budget through a system called &lt;strong&gt;Jetsam&lt;/strong&gt;. The budget depends on the device, community measurements put it under 1 GB on older iPhones and around 2 GB on mid-range ones. A leaderboard holding a few dozen 46 MB bitmaps alive walks into that limit within a couple of screen-heights of scrolling. &lt;strong&gt;The app is killed by the OS, not by your code.&lt;/strong&gt; There is no Dart exception and no useful stack trace, and your crash reporting tool shows an “out of memory session” at best. &lt;em&gt;That was our mysterious iOS crash.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One bug. Three ceilings. Three completely different symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Review Never Had a Chance&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now go back to that CircleAvatar snippet and ask: where in the diff is this bug?&lt;/p&gt;

&lt;p&gt;It is not there. The reviewer sees idiomatic widget code. The tests see an image that renders. CI is green. The 46.5 MB number exists only at runtime, on a real device, with real production images. The defect is spread across three systems whose owners never appear in the same pull request: the upload pipeline that stores 12-megapixel photos, the framework default that decodes at intrinsic size, and the platform policy that kills the process.&lt;/p&gt;

&lt;p&gt;This is also why the AI angle matters to us. An agent will write you this exact code, and it will be right by every static standard. An AI reviewer will approve it for the same reason a human does: &lt;em&gt;the evidence is simply not in the artifact being reviewed.&lt;/em&gt; As AI compresses the cost of writing code, the defects that survive migrate to the layers that neither the agent nor the reviewer can see. The cost does not disappear. It moves to a production incident three weeks later, on a device you do not own.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Fix Is One Parameter (and a Better One Upstream)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The immediate fix is embarrassingly small. Flutter lets you tell the decoder what size you actually need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;fit:&lt;/span&gt; &lt;span class="n"&gt;BoxFit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;cacheWidth:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;devicePixelRatioOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// CircleAvatar has no sizing hook, so wrap the provider:&lt;/span&gt;
&lt;span class="n"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;backgroundImage:&lt;/span&gt; &lt;span class="n"&gt;ResizeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;NetworkImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&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;With cacheWidth set, the same avatar decodes at 150 pixels instead of 4032. In our demo, that took each image from 46.51 MB down to 0.06 MB, roughly 775 times less memory, with zero visible difference in a 50-pixel circle. The whole leaderboard now fits in less than one megabyte of decoded images:&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%2F39rfqeopi0aufllo80gx.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%2F39rfqeopi0aufllo80gx.png" alt="Optimized leaderboard showing reduced image memory usage with efficient avatar caching" width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fixed build: same leaderboard, same photos, 0.8 MB of decoded images in total. The probe shows avatar #1 now decodes at 150x112, 0.06 MB.&lt;/p&gt;

&lt;p&gt;But honestly, the client-side parameter is the band-aid. The &lt;strong&gt;real fix is to never ship a 12-megapixel file to a 50-pixel widget in the first place&lt;/strong&gt;: serve resized images from your CDN or an image proxy. That also fixes what cacheWidth cannot, because the full-size download and the browser-side decode of the original file still happen either way. Fix it at the source and every client, including the ones you have not written yet, gets it for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Making Sure It Never Comes Back&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A bug that review cannot catch needs guardrails that do not depend on review.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The one we wish we had turned on earlier is &lt;a href="https://geekyants.com/en-in/hire-flutter-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;built into Flutter&lt;/strong&gt;&lt;/a&gt; itself. Set debugInvertOversizedImages to true in your debug builds, and the framework will flip and invert the colours of any image that was decoded significantly larger than its display size, and log the wasted bytes. Our leaderboard lit up like this:&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%2Fqj0runs1b00rwznp7qkc.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%2Fqj0runs1b00rwznp7qkc.png" alt="Flutter leaderboard with debugInvertOversizedImages highlighting oversized decoded avatars" width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;debugInvertOversizedImages in action: the framework flags every avatar that was decoded far larger than the size it is displayed at.&lt;/p&gt;

&lt;p&gt;Beyond that flag, a few habits close the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrap remote images in one shared widget&lt;/strong&gt; (an AppAvatar of your own) that requires a decode size, so the naive version cannot be written casually.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test on the smallest-RAM device you actually support&lt;/strong&gt;, with production images, not neat little asset placeholders.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a “random refresh on Safari” or an “app just closed” report comes in, &lt;strong&gt;put memory on the suspect list before routing and state management.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The most interesting thing about this bug is how ordinary the code was. No clever trick went wrong, no obscure API was misused. A default did exactly what it was documented to do, on inputs nobody in the pull request could see, on devices with limits nobody in the pull request was thinking about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://geekyants.com/en-in/ai" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;AI&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; &lt;em&gt;has made the code layer cheap. It has not made the other layers cheap.&lt;/em&gt; The asset pipeline, the decoder, the memory ceilings of a five-year-old iPhone, someone on the team still has to own those. The teams that move fastest with AI will not be the ones that generate the most code. They will be the ones who know exactly which questions the diff cannot answer, and go looking for the evidence themselves.&lt;/p&gt;

&lt;p&gt;The demo app used for every number and screenshot in this article is open source, and reproduces the whole story, including the Safari reload: &lt;a href="https://github.com/manuindersekhon/flutter-image-memory-demo" rel="noopener noreferrer"&gt;&lt;strong&gt;github.com/manuindersekhon/flutter-image-memory-demo&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ai</category>
      <category>webdev</category>
      <category>code</category>
    </item>
    <item>
      <title>What a PHP-to-NestJS Banking Migration Taught Us About Architecture, Security, and Trust</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 28 Sep 2026 06:12:26 +0000</pubDate>
      <link>https://dev.to/geekyants-inc/what-a-php-to-nestjs-banking-migration-taught-us-about-architecture-security-and-trust-3dae</link>
      <guid>https://dev.to/geekyants-inc/what-a-php-to-nestjs-banking-migration-taught-us-about-architecture-security-and-trust-3dae</guid>
      <description>&lt;p&gt;Every &lt;a href="https://geekyants.com/en-in/industry-expertise/banking-finance-insurance/digital-banking-transformation" rel="noopener noreferrer"&gt;&lt;strong&gt;digital banking platform&lt;/strong&gt;&lt;/a&gt; eventually hits the same wall: the monolith that shipped a decade ago still works, still processes real money for real account holders every day, and still cannot be touched without fear. Ours was a PHP application built with Laravel, serving multiple financial institutions from a single, tightly coupled codebase. It had grown feature by feature, bank by bank, for years. The rewrite wasn’t optional. Every new institution we onboarded meant another branch of conditional logic, another set of hacks specific to one bank bolted onto shared controllers, and another opportunity for one institution’s change to break another’s production environment.&lt;/p&gt;

&lt;p&gt;This article covers the architecture decisions we made while rewriting that platform as a multitenant system driven by configuration, and what the migration process taught us about verifying legacy behavior instead of assuming it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The core decision: separate “what the UI looks like” from “what the business logic does”&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The biggest architectural decision was splitting the platform into three layers instead of one: a thin frontend driven by configuration; a backend for frontend (BFF) that owns all business logic, session state, and vendor orchestration; and a config service that serves server-driven UI (SDUI) definitions per institution.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://geekyants.com/en-in/enterprise-system-modernization" rel="noopener noreferrer"&gt;&lt;strong&gt;legacy system&lt;/strong&gt;&lt;/a&gt;, “multitenant” meant if ($bank === 'X') scattered across views and controllers. Every institution’s quirks lived inside the same PHP files as everyone else’s, which meant every deployment carried the blast radius of every tenant at once. The new architecture inverts that. The mobile and web clients render forms, layouts, and copy from JSON served by the config service, merged through three layers: platform defaults, core banking provider defaults, then tenant-specific overrides. A new institution’s branding, field visibility, or feature flags become a config change, not a code change. The frontend contains no logic specific to an institution. It renders what the configuration defines.&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%2Fd2v82lhxrsp4oon5j5s9.jpg" 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%2Fd2v82lhxrsp4oon5j5s9.jpg" alt="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/81j3dmieondlhdpdx035.jpg" width="800" height="949"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That decision to push all business logic into a &lt;a href="https://geekyants.com/en-in/engineering/backend" rel="noopener noreferrer"&gt;&lt;strong&gt;backend service&lt;/strong&gt;&lt;/a&gt; and make the frontend a renderer let us migrate in stages. We could stand up new BFF endpoints one feature at a time behind the same configuration-driven client without a single cutover event. Legacy and new systems ran side by side for months, split by institution and feature, and nobody outside the engineering team could tell which backend was serving a given screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The security decision: stop letting the client hold the keys&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The second major decision came from an audit of the tokens flowing between the client and the backend. Like many systems that grow over time, the legacy platform’s API responses included internal identifiers and session tokens issued by the core banking system for a given member or account. These appeared in payloads that the client held and replayed on subsequent calls. The approach worked, but it meant an artifact sitting on the client carried a token minted by the core banking system itself, and every downstream service had to trust whatever the client sent back.&lt;/p&gt;

&lt;p&gt;We restructured this so the BFF never lets a core banking session token leave the server. On initial resolution, the BFF holds the real token on the server, keyed by its own session identifier, and the client sees only that opaque session UUID. Every subsequent request resolves the real token from server-side session state rather than trusting anything replayed from the client.&lt;/p&gt;

&lt;p&gt;The mechanical part of this change was straightforward. The challenge was scope: identifying every place where a value shaped like a real token was flowing, not just fields with an obvious name such as memberToken. We found aliased fields carrying the same sensitive value under a different property name. A search based on field names missed them because the value mattered, not the label attached to it.&lt;/p&gt;

&lt;p&gt;We also found flows that broke a simpler mental model. A couple of endpoints cached resolved session context across two separate HTTP requests: an MFA challenge and its resumption. A migration that always resolved the token from server-side session state would have broken authentication resumption the next time that challenge fired. Unit tests would not have exposed the failure because they typically mocked the layer where the gap lived.&lt;/p&gt;

&lt;p&gt;The fix in those cases was to carry an explicit internal object, never exposed to the client, through the cached challenge state and reseed server-side session context from it before resuming. This was a deliberate &lt;a href="https://geekyants.com/en-in/solution/design-system-development-service" rel="noopener noreferrer"&gt;&lt;strong&gt;design decision&lt;/strong&gt;&lt;/a&gt; rather than a mechanical rename, and one worth flagging for review rather than treating as a routine migration change.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The performance decision: let Redis carry the weight of session state&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Moving the real token off the client solved the trust problem, but it introduced a new one. The BFF now had to resolve that token, the member’s profile, and account-level context on every request that needed them. Calling the core banking system each time would have made the new platform slower than the one it was replacing. The third decision was to cache resolved session state rather than fetch it on every request.&lt;/p&gt;

&lt;p&gt;Once a session is established, the BFF writes the real token and the resolved member and account context into &lt;a href="https://geekyants.com/en-in/engineering/backend/product-backend-studio/redis-caching-services" rel="noopener noreferrer"&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/a&gt; under the opaque session identifier, with a time to live tied to the permitted session duration. Every later request in that session reads from Redis instead of paying the round-trip cost of a call to a core banking system that, in most deployments, is the slowest and most rate-limited dependency in the request path. What used to require a network call to an external system on every screen became an in-memory lookup, reducing fetch times for requests that depended on member or account context.&lt;/p&gt;

&lt;p&gt;The cache keys are never a flat string. Each one is built from three parts: what is being cached, which tenant it belongs to, and an optional subkey for the specific record. This structure prevents a cached profile for one institution from colliding with an identically shaped record for another. It also means an entire tenant’s cache can be cleared or inspected without touching anyone else’s data, which matters when a single Redis instance serves every institution on the platform.&lt;/p&gt;

&lt;p&gt;The same pattern caches the merged three-layer configuration for a tenant because recomputing that merge on every request would reduce the latency gains Redis provides. The result for account holders was fewer round trips per screen and lower dependence on fresh core banking lookups for each action.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The methodology lesson: documentation is something you build, not something you inherit&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One constraint shaped the migration process: there was no migration specification handed down from the old system. Nobody had written a document describing what the legacy application did feature by feature. The old codebase itself was the only complete record of the business rules. Institutional memory had faded, the original authors were gone, and the Blade templates and controllers were the closest thing to a specification that existed.&lt;/p&gt;

&lt;p&gt;The practice became clear: for every feature we migrated, we read the legacy implementation rather than relying on a summary and documented what we learned as we went. That growing set of documents became the migration’s specification, built one feature at a time instead of inherited on day one. The documentation became valuable, but we learned that it was not equivalent to the source itself.&lt;/p&gt;

&lt;p&gt;During one debugging session, three separate “bug fixes” based on our migration notes turned out to be wrong once checked against the legacy source. Each one moved behavior away from what the old system did in production rather than back toward it. In each case, the notes had conflated two separate legacy checks or described a distinction that sounded reasonable but that the original code didn’t make.&lt;/p&gt;

&lt;p&gt;The documentation was written in good faith by someone summarizing a page of PHP in a sentence, but the summary lost a detail that mattered.&lt;/p&gt;

&lt;p&gt;The corrective habit was clear but easy to skip under time pressure: treat every internal document, including the ones we wrote ourselves, as a working hypothesis about the legacy system rather than a fact about it. Before relying on a written description of “what the old system does” to justify a fix, reread the legacy implementation. Plausibility is not evidence. A behavior that “seems right” for a banking flow is worth only as much as the source code confirms.&lt;/p&gt;

&lt;p&gt;Every time the source and documentation disagreed, we corrected the documentation, so the record improved as the migration progressed. It never replaced the source as the final authority. The principle also works in the other direction. Not every divergence from legacy behavior is a bug. Some are intentional improvements that have received explicit approval. The discipline lies in knowing which is which before acting rather than assuming either case.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What carried over&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;None of these decisions were exotic. UI driven by configuration, a BFF that owns business logic, and server-side token indirection are well-established patterns. What made them work in a live, regulated, multitenant banking system was discipline in the implementation details: treating the legacy source, not a document about it, as the final authority; scoping security fixes by the value flowing through a field rather than its name; and building the migration documentation alongside the code instead of waiting for a finished specification that was never coming.&lt;/p&gt;

&lt;p&gt;The architecture gave us the seams to migrate safely. The process discipline kept the migration honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Building a Safer Path for Banking Modernization&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://geekyants.com/en-in/blog/how-us-fintech-companies-are-modernizing-legacy-banking-systems-without-full-rebuilds" rel="noopener noreferrer"&gt;&lt;strong&gt;Legacy banking systems&lt;/strong&gt;&lt;/a&gt; can be modernized without forcing a single high-risk replacement. Phased migration, clear service boundaries, secure session management, and careful verification of existing behavior can help teams modernize while protecting critical banking operations. For teams working through similar architecture challenges, explore our &lt;a href="https://geekyants.com/en-in/industry-expertise/banking-finance-insurance/digital-banking-transformation/core-banking-modernization" rel="noopener noreferrer"&gt;&lt;strong&gt;core banking modernization services&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nestjs</category>
    </item>
    <item>
      <title>Why Legacy Systems Make Business Growth More Expensive: Navigate A Smarter Path to Legacy Modernization</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 23 Sep 2026 11:53:55 +0000</pubDate>
      <link>https://dev.to/geekyants/why-legacy-systems-make-business-growth-more-expensive-navigate-a-smarter-path-to-legacy-57e</link>
      <guid>https://dev.to/geekyants/why-legacy-systems-make-business-growth-more-expensive-navigate-a-smarter-path-to-legacy-57e</guid>
      <description>&lt;p&gt;Learn how legacy systems make business growth more expensive and how edge-first modernization can remove constraints without replacing the existing system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Brands, Six Rebuilds, Until We Changed the Equation
&lt;/h2&gt;

&lt;p&gt;A North American restaurant group came to &lt;a href="https://geekyants.com/" rel="noopener noreferrer"&gt;GeekyAnts&lt;/a&gt; with six casual-dining brands and a digital estate. The system encountered challenges every time it needed to change as per the business requirements.&lt;/p&gt;

&lt;p&gt;There was an aging JSP web application, a separate native mobile app for each brand, and separate build and release pipelines for each of the systems. Each brand also had its own web and &lt;a href="https://geekyants.com/service/hire-mobile-app-development-services" rel="noopener noreferrer"&gt;mobile development teams&lt;/a&gt;. A change that should have been made once had to be implemented six times.&lt;/p&gt;

&lt;p&gt;Adding another brand took two to three months, and most of that time was going into creating another app, another pipeline, and duplicated work across the web and mobile teams. The core systems were not the issue since some of the routine tasks like ordering, menus, pricing, and availability were already working in a functional manner.&lt;/p&gt;

&lt;p&gt;The cost was actually spent in the existing digital layer around the system, where every additional brand meant another application, another pipeline, and more duplicated work. We started with a two-page &lt;a href="https://geekyants.com/blog/building-a-proof-of-concept-a-complete-guide-with-implementation-strategies" rel="noopener noreferrer"&gt;proof of concept&lt;/a&gt;. The question that needed to be answered was if the same components could genuinely be shared between web and native mobile. Once this assumption was tested, it gave us the basis to replace the edge while keeping the core systems in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing Changes to One Digital Platform Instead of Adding Six New Rebuilds
&lt;/h2&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%2Fiwd8dyvgkly71srxmxwv.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%2Fiwd8dyvgkly71srxmxwv.png" alt="Digital platform modernization architecture with edge components replaced while core systems are retained" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We focused on the layer where the duplication was happening and built one platform for web and mobile, so that the changes could be made once instead of across six separate brand applications. A shared component library removed repeated development work, while a single mobile app shell let the team configure and roll out new brands instead of building another app from scratch. Over-the-air updates also meant changes could reach users without repeating the full app release cycle for every brand.&lt;/p&gt;

&lt;p&gt;The first brand went live in 6-8 months. After that, a new brand was onboarded in 2-3 weeks instead of 2-3 months. The per-brand teams were consolidated into one web team and one mobile team, freeing them from rebuilding the same screen and flow.&lt;/p&gt;

&lt;p&gt;The seventh brand was added after the platform existed, without recreating the old setup. The goal was to remove the multiplication that came with every new brand.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does Legacy Infrastructure Become a Business Problem?
&lt;/h2&gt;

&lt;p&gt;Aging infrastructure alone is not the best reason to replace a system. In actuality, business constraints are one of the crucial reasons why legacy infrastructure needs to be replaced.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mckinsey.com/capabilities/tech-and-ai/our-insights/demystifying-digital-dark-matter-a-new-standard-to-tame-technical-debt" rel="noopener noreferrer"&gt;McKinsey&lt;/a&gt; estimates that technical debt can account for 20-40% of the value of an organization’s technology estate. So when a system becomes a problem, the business starts paying for its limitations. A release that once took two weeks can stretch into a quarter. Adding a new brand, region, or product can mean adding another technology stack. An integration task that should be a simple process becomes a whole project if the system doesn’t connect with it the right way.&lt;/p&gt;

&lt;p&gt;The same pattern shows up in other business operations where a compliance change that should take weeks takes months, engineering teams spend more time keeping duplicated experiences running than building the next product, and each new unit of growth costs more as the business has to repeat the work it has already done.&lt;/p&gt;

&lt;p&gt;The better question ideally would be “What is this system preventing the business from doing, and what is that constraint costing us?” because it changes the modernization conversation from replacing old technology to removing the things that hold business growth back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Did We Modernize Edge Before the Core System?
&lt;/h2&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%2F5zv8t2xq35czxn9gz9mg.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%2F5zv8t2xq35czxn9gz9mg.png" alt="Edge-first modernization diagram with before and after architecture with shared web and mobile components" width="800" height="683"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a core system starts holding the business back, the first instinct would be to replace it, which may be a risky process.&lt;/p&gt;

&lt;p&gt;Most of the business processes may not be documented in one place, as some may exist in code, some in old integrations, and some may only be known by the people who have worked with the system for years.&lt;/p&gt;

&lt;p&gt;The edge is different. It is where customers, employees, and partners interact with the business: &lt;a href="https://geekyants.com/service/hire-web-app-development-services" rel="noopener noreferrer"&gt;web&lt;/a&gt; and mobile experiences, APIs, integrations, workflows, automation, and deployment pipelines.&lt;/p&gt;

&lt;p&gt;Modernizing the edge gives the businesses enough room to be adaptive to changes without immediately disrupting the logic on which the systems run. You can improve how customers place an order, connect a new channel, or streamline an employee workflow without interrupting the system that handles ordering or pricing tasks.&lt;/p&gt;

&lt;p&gt;The risk, however, changes as you move closer to the core system. The more business logic and dependencies you have to uncover, the more you risk finding rules nobody remembered were there.&lt;/p&gt;

&lt;p&gt;Needless to say, &lt;strong&gt;modernization risk increases with the amount of business logic you have to rediscover.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven Decisions To Be Considered for Modernization of Legacy Systems
&lt;/h2&gt;

&lt;p&gt;Every part of the legacy system can have a different path forward. Depending on what it does and where it creates friction, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retire&lt;/strong&gt; the system when nothing depends on it anymore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retain&lt;/strong&gt; when it works and creates no business constraint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expose&lt;/strong&gt; when the capability is useful but trapped behind poor interfaces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Augment&lt;/strong&gt; when the core works but the surrounding experience does not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-platform&lt;/strong&gt; when the application works but the underlying runtime has become the problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refactor&lt;/strong&gt; when the business logic is worth keeping but the architecture prevents change or scale.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replace&lt;/strong&gt; when the economics no longer make sense or the system's business logic no longer matches how the company operates.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Modernization Should Create Room for Business Growth
&lt;/h2&gt;

&lt;p&gt;The best place to begin the modernization process often starts from navigating which business process is actually hindering its growth. The real opportunity in &lt;a href="https://geekyants.com/enterprise-system-modernization" rel="noopener noreferrer"&gt;legacy modernization&lt;/a&gt; lies in making &lt;strong&gt;room for business’s growth without creating unnecessary risks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Planning a legacy modernization initiative? Talk to GeekyAnts about identifying what is actually constraining growth and modernizing it without replacing the existing system.&lt;/p&gt;

</description>
      <category>legacycode</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
