The Scheduling Boundaries Behind Responsive UI

Decision tree to decide among Promises, setTimeout and requestAnimationFrame

This is the last article in a series on how JavaScript actually runs. You can read the full series here or on my website.

We now know how the event loop and rendering pipeline behave.

The browser:

  • Runs a macrotask to completion.
  • Drains all microtasks.
  • Executes any scheduled requestAnimationFrame callbacks.
  • Drains all microtasks.
  • Performs layout and paint to produce the next frame.
  • Moves on to the next macrotask.

Given that environment, how should we write UI code?

Long Tasks Block Everything

If you want the UI to stay responsive, your tasks must yield quickly.

Consider this:

button.addEventListener("click", () => {
  const start = performance.now();

  while (performance.now() - start < 3000) {
    // busy loop for 3 seconds
  }

  console.log("done");
});

Once the button is clicked, the page becomes unresponsive. The click handler is a macrotask and nothing can interrupt it. Everything else has to wait: there is no re-rendering, no new input, no requestAnimationFrame callbacks.

Long-running tasks monopolize the main thread. While they run, rendering pauses, input waits, animations stall and timers are delayed. Responsive UI depends on cooperation.

Microtasks Do Not Yield to Rendering

Chaining promises look like a way to break work into pieces:

button.addEventListener("click", () => {
  Promise.resolve()
    .then(() => heavyWork())
    .then(() => {
      status.textContent = "Halfway...";
    })
    .then(() => moreHeavyWork())
    .then(() => {
      status.textContent = "Done";
    });
});

However, each .then() callback becomes a microtask when its associated Promise resolves. Because the browser must drain the entire microtask queue before rendering, chaining Promises does not create render opportunities. As a result, the user sees nothing until "Done" shows up on screen.

If you want to let the browser render, you must introduce a scheduling boundary:

button.addEventListener("click", () => {
  Promise.resolve()
    .then(() => heavyWork())
    .then(() => {
      status.textContent = "Halfway...";

      return new Promise(resolve => {
        // setTimeout used to introduce a scheduling boundary
        setTimeout(resolve, 0); 
      });
    })
    .then(() => heavyWork())
    .then(() => {
      status.textContent = "Done";
    });
});

Alternatively, we can consider:

button.addEventListener("click", () => {
  Promise.resolve()
    .then(() => heavyWork()
    .then(() => {
      status.textContent = "Halfway...";

      return new Promise(resolve => {
        // requestAnimationFrame used to introduce a scheduling boundary
        requestAnimationFrame(resolve); 
      });
    })
    .then(() => heavyWork()
    .then(() => {
      status.textContent = "Done";
    });
});

Both approaches work because a .then() callback is only queued once its associated Promise resolves. By returning a Promise that resolves later, we delay when the next microtask is created. setTimeout yields to the next macrotask, while requestAnimationFrame yields to the next frame.

Choosing Between Promises, setTimeout and requestAnimationFrame

These mechanisms signal different intentions to the browser. They are not interchangeable.

Use a Promise when you need to continue work immediately after the current macrotask completes, but before the browser moves on. They are ideal for continuing work that logically depends on previous work. They help to preserve order, transform results and update state after completion. They are not a yielding mechanism.

Use setTimeout when you need to create a real scheduling gap. They are useful for breaking up long computation, deferring non-critical work and yielding cooperatively. They are general purpose yields.

Use requestAnimationFrame when you are performing visual updates. It is ideal for animations and layout-sensitive work.

Decision tree to decide among Promises, setTimeout and requestAnimationFrame
Different scheduling APIs solve different coordination problems in the browser event loop.

Align Visual Updates to Frames

Consider this approach:

document.addEventListener("mousemove", (event) => {
  box.style.left = event.clientX + "px";
});

If the mouse fires 200 events per second, this code attempts 200 DOM updates per second. But on most displays, the refresh happens about 60 times per second. Visual work that exceeds that rate is simply wasted.

Instead, consider:

let latestX = 0;
let scheduled = false;

document.addEventListener("mousemove", (event) => {
  latestX = event.clientX;

  if (!scheduled) {
    scheduled = true;

    requestAnimationFrame(() => {
      box.style.left = latestX + "px";
      scheduled = false;
    });
  }
});

We use requestAnimationFrame to update at most once per frame, no matter the rate at which input can fire.

Respect the Frame Budget

requestAnimationFrame guarantees alignment to the frames but it does not guarantee smoothness. On a 60fps display, the browser has roughly 16ms per frames. That 16ms must include all JavaScript and rendering work.

If the JavaScript executed alone takes longer than that, the browser cannot complete rendering in time:

requestAnimationFrame(() => {
  const start = performance.now();

  while (performance.now() - start < 40) {
    // 40ms of work
  }
});

This callback runs before the browser renders but it blocks for 40ms and therefore exceeds the frame budget. Since the browser cannot display partial frames, if the 16ms window is missed, that frame is dropped. Instead of rendering at 60 frames per second, the browser renders less frequently: Animations can appear jerky, motion uneven and interactions delayed.

So while requestAnimationFrame helped with alignment, we must still finish the frame work within the frame window. Either work fits inside the budget or it is spread across multiple frames. For instance, this could mean animating in steps or deferring non-critical computation.

Responsive UI requires both correct scheduling and work that fits inside the budget.

Guard Against Stale Asynchronous Work

Asynchronous code creates delays. During this window, state can change:

function loadData() {
  fetch("/data")
    .then(response => response.json())
    .then(data => {
      render(data);
    });
}

This looks harmless but imagine the user clicking twice quickly and loadData() is called twice in succession. If the second request finishes first, the first request would render stale data and the UI would then be incorrect.

One common pattern is to guard against outdated work:

let currentRequestId = 0;

function loadData() {
  const id = ++currentRequestId;

  fetch("/data")
    .then(response => response.json())
    .then(data => {
      if (id !== currentRequestId) return;
      render(data);
    });
}

Now each request captures its own response and only the most recent request is allowed to update the UI.

Designing With the Browser, Not Against It

Responsive UI emerges from working within the browser’s execution model.

In practice, that often means:

  • Keeping tasks short so the browser can continue scheduling
  • Remembering that microtasks do not yield to rendering
  • Aligning visual updates to frame boundaries
  • Ensuring that work fits within the frame budget.
  • Verifying that delayed work is still relevant before applying it

I started this series because I had code that used Promises, setTimeout, and requestAnimationFrame. They all felt “asynchronous” and interchangeable. Turns out they weren’t.

Good UI code knows which scheduling boundary to use and when.

This article was originally published on my website.

Cache Stampede in Front of the CDN: Origin Server Loading Wars

Introduction: What is Cache Stampede in Front of a CDN?

In today’s high-performance web applications, Content Delivery Networks (CDNs) play an indispensable role. They reduce latency by delivering content to users from geographically close servers, lower bandwidth costs, and alleviate the load on origin servers. However, there are specific scenarios that even CDNs struggle to handle.

One of these scenarios is known as “Cache Stampede,” which can lead to serious issues, especially when it occurs in front of a CDN. A Cache Stampede happens when thousands or millions of concurrent requests are directed straight to the origin server for content that is either missing from the cache or has expired. This situation represents “loading wars” for origin servers and can lead to severe performance issues, including service outages. In this post, we will take a deep dive into the Cache Stampede problem occurring in front of CDNs, understand its causes, and detail the strategies we can use to prevent or mitigate this destructive impact.

What is Cache Stampede and Why Does It Matter?

Cache Stampede is a situation in a caching system (in this case, a CDN cache) where, at the moment a specific piece of content is not found in the cache or has expired, a large number of concurrent requests for that content reach the backend origin server simultaneously. This sudden and uncoordinated flow of requests can overload the origin server, causing it to slow down, return errors, or crash entirely.

The importance of this situation stems from its criticality to the scalability and performance of modern web infrastructures. High-traffic websites and applications rely heavily on CDNs; therefore, a Cache Stampede event can directly impact user experience and lead to business losses. The load reduction and acceleration benefits, which are the primary goals of a CDN, can completely vanish during a Cache Stampede.

The Core Mechanism of Cache Stampede

Cache Stampede typically occurs through the following steps:

  1. Cache Miss: A client requests a resource (image, HTML page, API response, etc.) from the CDN. However, this resource is either not in the CDN’s cache at all or has expired.
  2. Forwarding to Origin: The CDN forwards this request to the origin server. The origin server begins a process to generate or retrieve the content.
  3. Concurrent Requests: Exactly at this moment, thousands of other clients request the same resource. These requests also experience a cache miss on the CDN and are forwarded to the origin server at the same time.
  4. Origin Overload: While trying to respond to this intense volume of simultaneous requests, the origin server experiences resource exhaustion (CPU, memory, database connections). This leads to the server slowing down, returning error responses, or becoming unresponsive.
  5. Degraded User Experience: Users encounter slow-loading pages, errors like “500 Internal Server Error,” or completely inaccessible services.

This situation can spiral out of control very quickly, especially with popular content or during peak campaign periods. The origin server’s inability to handle this sudden load can create a domino effect, impacting the entire system.

The Relationship Between CDNs and Cache Stampede

While CDNs are designed to reduce the risk of Cache Stampede, they can actually worsen the problem under certain conditions. Massive CDN networks serving millions of users have the potential to forward a huge number of requests for the same content to the origin server simultaneously in the event of a single cache miss.

CDNs generally have caching layers and store content at points closer to users. This usually significantly reduces the load on the origin server. However, when a piece of content drops out of the cache or new content is requested for the first time, all of the CDN’s edge locations might start hitting the origin at once.

How CDNs Can Trigger a Cache Stampede

  • Cold Cache Condition: When new content is published or when a request first hits a CDN region, the cache is empty. While the first request goes to the origin, other concurrent requests are forwarded to the origin in the same way.
  • Cache Expiration: When the TTL (Time-To-Live) specified in a content’s Cache-Control header expires, the CDN sends a validation request (usually with If-None-Match or If-Modified-Since headers) to the origin to check if the content is up to date. If many CDN edge servers attempt to validate content with an expired TTL at the same time, this can also create a Stampede.
  • CDN Cache Purge: A manual or automatic cache purge performed by system administrators can empty all CDN caches. In this case, all subsequent requests are directed to the origin, creating a massive Stampede risk.
  • Multi-Tier Caching: Some CDNs offer features like Origin Shield or Tiered Caching, ensuring that fewer CDN servers communicate with the origin. However, when the internal caches of this tiered architecture are emptied or expire, a Stampede risk still exists, just through a smaller number of “origin-facing” servers.

ℹ️ CDN and Scale

The larger the scale of a CDN, the greater the potential impact of a Cache Stampede. A global CDN network serving millions of concurrent users can send millions of simultaneous requests to your origin server in the event of a single cache miss. This can turn a simple caching issue into a global service outage.

Cache Stampede Scenarios and Triggers

Cache Stampede can be triggered under various scenarios, each with its own dynamics. Understanding these triggers is critical for developing effective defense strategies.

Cold Cache Conditions

Cold cache refers to situations where a piece of content has never been in the CDN cache before or has recently been cleared from the cache. These scenarios are among the most common triggers for Cache Stampede.

  • New Content Distribution: When a new article, product page, or media file is added to a website, this content naturally doesn’t exist in any CDN cache initially. When requested for the first time, all requests are directed to the origin server.
  • CDN Cache Purge: A cache purge performed by an administrator invalidates all or specific content in the CDN. This causes the entire cache to become “cold,” and all subsequent requests go to the origin. This can happen particularly after a major deployment or bug fix.
  • Long-Tail Content: Rarely accessed content (long-tail content) may not stay in CDN caches for long. When this content suddenly becomes popular or is highlighted by a campaign, it can be requested by many people at once, leading to a Cache Stampede.

Cache Expiration and Revalidation

When the cache duration expires (TTL – Time-To-Live), the CDN communicates with the origin server to see if the content is up to date. This validation process can also cause a Cache Stampede.

  • TTL Expiration: When the max-age specified in an object’s Cache-Control header expires, the CDN sends a request to the origin to revalidate or completely re-fetch the object. If many concurrent users are requesting the same object and the object’s TTL expires at the same time, a massive validation or re-fetch load can hit the origin server.
  • **Cache-Control: no-cache or must-revalidate:** These headers require the CDN to always check the freshness of the content with the origin. no-cache can actually cache the content but mandates that it always be validated. must-revalidate forces the content to be validated by the origin after the cache expires before being served to users. Such policies increase the risk of Cache Stampede by leading to frequent validation requests.

Flash Crowds

Unexpected or unpredictable traffic spikes can strain CDN caching mechanisms and lead to a Cache Stampede.

  • Viral Content: A news story, video, or article suddenly going viral can cause millions of people to request the same content in a short time. If this content is new or has dropped out of the cache, it puts a massive load on the origin server.
  • Marketing Campaigns and Promotions: Marketing activities such as a new product launch, a major discount campaign, or television advertisements create a targeted and intense flow of traffic. If the homepages or product detail pages shown during these campaigns are not in the cache, a Cache Stampede may be inevitable.
  • DDoS-like Situations: Intense and distributed bot traffic, even if not malicious, can create a Cache Stampede effect. Bots can target different pages of the site or a specific page, causing cache misses and overwhelming the origin server.

⚠️ The Risks of Cache Purging

Great care should be taken before performing a large-scale cache purge. This process can leave your site completely vulnerable to Cache Stampede risks for a short period. If possible, gradual purging or smart cache invalidation methods should be preferred.

Defense Strategies Against Cache Stampede

A range of strategies exists to prevent or reduce the destructive effects of Cache Stampede. These strategies can be applied both at the CDN layer and on the origin server side.

Coordinated Cache Refresh

This strategy aims to prevent multiple clients from requesting the same content from the origin at the same time.

  • Cache-Control Headers: HTTP Cache-Control headers are critical for determining how CDNs and browsers cache content and when they refresh it.

    • **stale-while-revalidate:** This header allows the CDN to continue serving a stale copy of an object to users even after its cache duration has expired, while it fetches the updated copy from the origin in the background. This prevents the user from waiting and avoids a sudden load on the origin.

      Cache-Control: public, max-age=3600, stale-while-revalidate=60
      

      In this example, the content is considered fresh for 1 hour (max-age=3600). During the next 60 seconds (stale-while-revalidate=60), the CDN continues to serve the old content while fetching the new content from the origin in the background.

    • **stale-if-error:** This header allows the CDN to continue serving stale content from its cache when there is a problem with the origin server (e.g., when it returns a 5xx error code). This buys time for the origin server to recover.

      Cache-Control: public, max-age=3600, stale-if-error=86400
      

      In this example, the CDN can serve stale content for 24 hours (stale-if-error=86400) when the origin server returns an error.

  • CDN Features: Many modern CDNs offer specific features to prevent Cache Stampede.

    • Origin Shield / Tiered Caching: This feature ensures that all edge locations in the CDN network go to a few central “shield” or “mid-tier” cache servers instead of going directly to the origin server. This significantly reduces the total number of requests reaching the origin server.
    • Request Collapsing / Deduplication: This feature merges multiple requests for the same content at the same time into a single origin request. While the first request goes to the origin, other requests wait for this response, and once the response arrives, it is served to all waiting requests.

Request Collapsing / Deduplication

Request collapsing is one of the most effective CDN mechanisms for preventing Cache Stampede. When a CDN edge server sends the first request to the origin for a resource not in its cache, it starts “holding” all subsequent requests for the same resource. When the response arrives from the origin, it caches this response and serves it to all waiting requests simultaneously. This way, even thousands of concurrent requests only reach the origin once.

This feature may be offered automatically by your CDN provider or may require specific configuration. Services like Cloudflare’s “Argo Smart Routing” and Akamai’s “SureRoute” include these types of optimizations.

Graceful Degradation and Fallback Mechanisms

When the origin server is overloaded or returns an error, fallback mechanisms are important to prevent the entire system from crashing.

  • Serving Stale Content: The stale-if-error header mentioned above is a good example of this. When the origin server is unreachable or returns an error response, continuing to serve stale content from the CDN’s cache is better than losing the user experience entirely.
  • Static Fallbacks: For critical content, you can prepare a simple, static HTML page or an error message to be displayed when the origin server cannot be reached. This at least prevents users from seeing a completely blank page or a browser error.

Origin Server Optimizations

Even if we cannot completely prevent Cache Stampede, some optimizations can be made so that the origin server can better handle such loads.

  • Faster Response Times: The origin server generating or retrieving content as quickly as possible reduces the impact of Cache Stampede. Optimizing database queries, making code efficient, and making heavy processes asynchronous can help in this regard.
  • Load Balancing and Auto-Scaling: Using a load balancer behind the origin servers and automatically starting new server instances based on demand (auto-scaling) increases resilience against sudden traffic spikes.
  • Rate Limiting: Implementing a rate limiting mechanism on the origin server that restricts the number of requests from a specific IP address or user can prevent malicious or excessive requests from overwhelming the server.

Proactive Cache Management

One of the best ways to prevent Cache Stampede is to manage caches proactively.

  • Pre-fetching / Pre-warming: “Warming up” critical content or newly published content in CDN caches before real users request it. This can be done with an automated script or an API call. For example, when a new article is published, a bot can be run to request the URL of this article from the CDN at certain intervals.
  • Smart TTL Strategies: Determining appropriate max-age and stale-while-revalidate values based on the nature of the content. It makes sense to use short TTLs for frequently updated content and long TTLs for static and rarely changing content.
  • Soft Purging Instead of Cache Invalidation: Instead of completely emptying the cache, using methods like “soft purge” or “tag-based invalidation” that indicate to the CDNs that the content is “stale” and needs to be revalidated. This can allow the CDN to continue serving old content while fetching new content in the background.

Observation and Monitoring

Continuous monitoring of your system is vital for an effective defense.

  • CDN Logs and Metrics: Regularly review the logs and performance metrics (cache hit ratio, origin request count, error rates) provided by your CDN provider. Abnormal increases or decreases can be early signs of a potential Cache Stampede.
  • Origin Server Metrics: Closely track metrics such as CPU usage, memory consumption, network traffic, database connections, and error rates of your origin server. Sudden jumps in these metrics can indicate that the origin server is under a Cache Stampede effect.
  • Anomaly Detection: Set up monitoring and alerting systems that can automatically detect unexpected changes in these metrics. This way, you can intervene before or in the early stages of a Cache Stampede.

Implementation Examples and Best Practices

Let’s provide some examples and best practices on how to apply the strategies we’ve discussed in real-world scenarios.

Cache-Control Header Usage

Correctly configuring Cache-Control headers on your web server or application layer is the first step in preventing Cache Stampede.

Example 1: Blog Post (Dynamic Content)
A blog post can generally be considered fresh for 5 minutes, but the old copy can be served for a longer period while it is updated in the background.

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=300, stale-while-revalidate=3600
Expires: Tue, 21 Apr 2026 10:05:00 GMT

In this example, the content is considered fresh for 5 minutes (max-age=300). At the end of this period, for the next 1 hour (stale-while-revalidate=3600), the CDN will continue to serve the old copy while fetching a new copy from the origin.

Example 2: Static Resources (Images, CSS, JS)
Static files can usually be kept in the cache for a very long time and are served via a new URL using versioning when a change occurs.

HTTP/1.1 200 OK
Content-Type: image/jpeg
Cache-Control: public, max-age=31536000, immutable
Expires: Wed, 21 Apr 2027 09:00:00 GMT

The max-age=31536000 (1 year) and the immutable directive ensure that browsers and CDNs keep this file in the cache for a very long time and indicate that it will not change once downloaded.

CDN Provider Specific Settings

Major CDN providers offer special tools and settings to manage Cache Stampede.

  • Cloudflare: Cloudflare’s “Always Online” feature attempts to serve an archived copy of your site when the origin server is unreachable. The “Tiered Cache” feature provides a layered caching similar to Origin Shield. You can also easily apply custom Cache-Control headers like stale-while-revalidate with “Cache Rules.”
  • Akamai: Akamai offers advanced features like “Edge Load Balancing” and “Origin Shield.” Additionally, optimizations like “Adaptive Image Compression” reduce the amount of data pulled from the origin, lightening the load.
  • AWS CloudFront: CloudFront allows you to customize cache behavior with “Lambda@Edge.” You can dynamically change Cache-Control headers, apply stale-while-revalidate, or add logic similar to request collapsing. Origin Shield is also available for your CloudFront Distributions.

💡 Review CDN Documentation

Examine your CDN provider’s documentation in detail to learn what specific features and configurations they offer to prevent Cache Stampede. Often, ready-made tools are available to solve these types of problems.

Example Scenario: Cache Warming for a New Product Launch

A large e-commerce site is about to launch a highly anticipated new product. Thousands of people will visit the product page at the moment of launch. What can be done to prevent a Cache Stampede?

  1. Pre-Launch Preparation: The product page and related images are published in the background a few hours before the launch but are not public (e.g., with a noindex tag or a temporary redirect).
  2. Cache Warming Script: A simple script written in a language like Python or Node.js requests the URL of the product page and the URLs of related images from the CDN at certain intervals (e.g., every 30 seconds).

    import requests
    import time
    
    urls_to_warm = [
        "https://cdn.example.com/new-product-page",
        "https://cdn.example.com/images/new-product-hero.jpg",
        "https://cdn.example.com/images/new-product-gallery-1.jpg",
        # ... other critical URLs
    ]
    
    def warm_cache(url):
        try:
            response = requests.get(url, timeout=5)
            if response.status_code == 200:
                print(f"Successfully warmed: {url}")
            else:
                print(f"Failed to warm {url}: Status {response.status_code}")
        except requests.exceptions.RequestException as e:
            print(f"Error warming {url}: {e}")
    
    if __name__ == "__main__":
        print("Starting cache warming process...")
        while True:
            for url in urls_to_warm:
                warm_cache(url)
            print("Warming cycle complete. Waiting 30 seconds...")
            time.sleep(30) # Repeat every 30 seconds
    
  3. Launch Moment: When the launch begins, the CDN caches are already full, and most requests are served directly from the CDN. The origin server only encounters a small number of “cold cache” or validation requests.

  4. Monitoring: CDN and origin server metrics are carefully monitored during the launch. Any anomalies are addressed quickly.

This approach significantly reduces the risk of Cache Stampede during sudden traffic spikes and protects the performance of your origin server.

Conclusion

Cache Stampede occurring in front of a CDN is a serious performance and reliability threat for modern web applications. These “origin server loading wars” can lead to service outages and poor user experiences if correct strategies are not applied. However, it is possible to prevent this problem at its source with techniques like request coalescing, stale-while-revalidate, lock-based regeneration, and probabilistic early expiration. The most important lesson: treat cache invalidation policy not as a “major event” but as a daily design decision.

LearnCurator – I built a YouTube tutorial search engine that filters videos, ranks by AI‑analysed comments

LearnCurator – Stop wasting time on bad YouTube tutorials

I love learning from YouTube, but I kept running into the same problems:

  • Shorts mixed into search results (too short to teach anything)
  • Non‑English/Hindi videos (I don’t speak Tamil or Telugu)
  • Outdated tutorials (still showing 4‑year‑old videos even if better ones exist)
  • No way to tell quality until I’ve watched 5 minutes

So I built LearnCurator – a smart search engine for YouTube tutorials.

🔍 How it works

  1. Initial engagement score The app first computes a base score for each video using: -engagementRatio = likes / views (capped at 1.0) -recencyFactor based on publish age

In youtubeService.js: -score = engagementRatio * 0.7 + recencyFactor * 0.3 -So videos with high like/view ratio and recent publish date rank higher.

  1. Sentiment and final score After getting the top candidate videos, it enriches them with sentiment analysis and a popularity boost.

Final score formula: finalScore = engagementRatio * 0.3
recencyFactor * 0.1
sentimentScore * 0.3
viewBoost * 0.3

🤖 AI‑powered comment analysis

I integrated Gemini 1.5 Flash to analyse YouTube comments. The AI counts positive vs negative feedback and adds a sentiment score to each video. This helps surface content that the community actually loved – not just clicked on.

👍👎 User feedback

Every video card has 😍 😐 😞 buttons. Users can rate a tutorial and leave optional comments. I built a private admin dashboard where I can see real‑time feedback, average ratings, and trending complaints.

🌐 Live demo

👉 [https://learncurator.netlify.app/]

Try searching for “JavaScript promises” or “React hooks” – you’ll only get videos, no shorts, all longer than 5 minutes, ranked by quality.

🛠️ Tech stack

  • Frontend: HTML, CSS, vanilla JS (responsive, works on mobile)
  • Backend: Netlify Functions (Node.js)
  • APIs: YouTube Data API v3, Gemini 1.5 Flash
  • Database: Supabase (feedback storage + caching)
  • Deployment: Netlify (frontend + functions)

📊 What I learned

  • How to handle YouTube API quotas and caching
  • Using Gemini for sentiment analysis on real‑world data
  • Building a secure admin dashboard without a traditional backend
  • Deploying a full‑stack app entirely on Netlify’s free tier

🚀 What’s next

  • Add transcript relevance scoring (search inside videos)
  • Personalised recommendations based on past feedback
  • Dark mode (because every app needs it 😄)

I’d love your feedback. Try it out and let me know what you think!

If you’re a learner tired of low‑quality tutorials, or a developer curious about AI + YouTube, give LearnCurator a spin.

Running Qwen3.6-27B on a 16GB M1 MacBook Pro: A Practical Engineer’s Guide

Running Qwen3.6-27B on a 16GB M1 MacBook Pro: A Practical Engineer’s Guide

Running a 27B model on a 16GB M1 MacBook Pro sounds a little unfair to the machine.

I get the appeal, though. You want a capable local model, no cloud dependency, no API bill, and more privacy when testing prompts, code snippets, architecture notes, or security workflows. The M1 MacBook Pro is still a solid machine, but with a 27B-class model, the limiting factor is not enthusiasm or CPU performance. It is memory.

This guide is written from a practical engineering point of view: what I would try first, what I would avoid, and how I would tune the setup so the Mac remains usable.

The recommended starting point here is temperature 0.1 for more deterministic first-run testing.

One important clarification: this is not a production-serving guide. The official Qwen3.6 serving examples assume a very different environment from a 16GB laptop. This guide is specifically about testing whether a constrained 16GB Apple Silicon machine can run a quantized version in a useful way for short engineering prompts.

First, the honest reality check

Qwen3.6-27B is a large language model with about 27 billion internal parameters. Because it is much larger than a 7B model, it needs significantly more memory to run locally.

On a 16GB Apple Silicon Mac, that is a hard constraint.

The model weights, KV cache, Python runtime, macOS, and your open applications all share the same unified memory pool. If memory pressure rises too high, macOS starts swapping to SSD. Once swapping starts, generation speed can degrade sharply.

So yes, with the right quantized build, you may be able to run Qwen3.6-27B locally on a 16GB M1 MacBook Pro. But it should be treated as a constrained experiment, not the same experience as running a smaller 7B model.

Expect trade-offs:

  • Use an aggressively quantized build, such as a 3-bit or similarly compact variant.
  • Keep prompts and outputs short during initial testing.
  • Close memory-heavy applications like Chrome, Docker Desktop, Slack, Teams, and large IDEs.
  • Do not attempt to use the full advertised context window on a 16GB machine.
  • Expect slower generation and more tuning than you would see on a cloud GPU or a higher-memory Mac.

For engineers, this is the key mindset: the goal is not to run the biggest model possible; the goal is to keep it usable.

Why MLX is the right starting point

For Apple Silicon, start with MLX.

MLX is Apple’s machine learning framework for Apple Silicon. The important part is that it works naturally with Apple’s unified memory architecture. The mlx-lm project is designed to run and fine-tune language models on Apple Silicon and can load compatible models from Hugging Face.

You can also try Ollama or LM Studio if you prefer a GUI or simpler workflow, but for this specific hardware constraint, MLX gives you the best chance of squeezing useful performance out of the Mac.

Recommended setup

Create a clean Python environment:

python3 -m venv mlx-qwen
source mlx-qwen/bin/activate
python -m pip install --upgrade pip
pip install mlx-lm transformers

Check that the command is available:

mlx_lm.generate --help

This also matters because MLX-LM CLI options can change across versions. Before you run the final command, confirm that your installed version supports the flags you plan to use:

mlx_lm.generate --help | grep -E "temp|max-tokens|max-kv-size"

If a flag is missing or named differently, trust your installed CLI help output over any blog post, including this one.

Pick the smallest practical quantized model

Do not start with BF16. A BF16 27B model is not realistic for a 16GB Mac.

Look for an MLX-compatible Qwen3.6-27B model that is aggressively quantized, ideally around 3-bit or similarly compact. A 4-bit model may run, but on 16GB it can still be uncomfortable once you add prompt context, KV cache, runtime overhead, and normal macOS memory usage.

Search for model names that include patterns like:

Qwen3.6-27B
MLX
3bit
4bit
IQ3
Q3

A safer first test is a 3-bit or IQ3-style quant. The quality may be lower than 4-bit or BF16, but it gives your 16GB machine a better chance to avoid heavy swapping.

I would avoid downloading random community builds blindly. Check the model card, file size, quantization method, license, and user comments before running it.

First test command

Once you have chosen a compatible MLX model, run a small prompt first:

mlx_lm.generate 
  --model <your-qwen3.6-27b-mlx-quantized-model> 
  --prompt "Give me a concise checklist for hardening an exposed SSH server." 
  --max-tokens 200 
  --temp 0.1 
  --max-kv-size 1024

Replace:

<your-qwen3.6-27b-mlx-quantized-model>

with the Hugging Face model repo or local model path you selected.

Why these settings?

  • --max-tokens 200 keeps output short.
  • --temp 0.1 makes the output more focused and repeatable for first-run testing.
  • --max-kv-size 1024 limits KV cache memory growth.
  • The prompt is short enough to test the model without punishing the machine.

The temperature 0.1 recommendation is intentionally conservative. It is useful for first-run validation because it keeps answers more predictable and reduces output variance. It is not necessarily the best quality setting for every Qwen3.6 use case. Once the model is stable on your machine, you can experiment with the model-card sampling recommendations.

The KV cache setting has a trade-off too. A smaller KV cache helps memory pressure, but very small values can weaken long-context coherence. Start small to prove the model runs, then increase only if memory pressure stays acceptable.

If the command fails because your installed MLX-LM version uses a different sampling flag, run:

mlx_lm.generate --help

and adjust the temperature option based on your installed version.

Watch memory pressure, not just token speed

Open Activity Monitor and check the Memory Pressure graph.

Green is good. Yellow means you are close to the edge. Red means the machine is struggling.

If the model starts generating extremely slowly, do not immediately blame Qwen. Check these first:

  • Are Chrome, Docker Desktop, Slack, Teams, or a large IDE open?
  • Is the prompt too long?
  • Is --max-tokens too high?
  • Is --max-kv-size too large?
  • Did you accidentally use a BF16 or large 4-bit model?
  • Is macOS swapping heavily?

On my recommended 16GB workflow, I would keep the first few tests boring and controlled. Short prompt. Short output. Temperature 0.1. Low KV cache. Then increase one setting at a time.

Use chat mode carefully

You can also run an interactive chat session:

mlx_lm.chat --model <your-qwen3.6-27b-mlx-quantized-model>

This is convenient, but it can become slower as the chat history grows. For a 16GB machine, long conversations are not free. If performance drops, restart the session.

For engineering work, I prefer short single-shot prompts when testing a large local model:

Review this Terraform IAM policy and identify risky permissions. Keep the answer under 10 bullet points.

That style is easier on memory and usually gives better signal.

Should you disable thinking mode?

If your selected Qwen build and runtime support thinking mode, be careful with it.

Thinking mode can be helpful for debugging, architecture analysis, and deeper reasoning. It can also generate more tokens and increase latency. On a 16GB MacBook Pro, that matters.

Use thinking mode when you need it:

  • Code reasoning
  • Security architecture review
  • Complex troubleshooting
  • Multi-step design trade-offs

Turn it off for lighter work:

  • Short summaries
  • Blog outlines
  • Quick command explanations
  • Simple Q&A
  • Rewriting text

For day-to-day local use, non-thinking mode plus conservative sampling will usually feel more responsive.

The better engineering option: consider an MoE model

If your real goal is a usable local assistant, seriously consider a Mixture-of-Experts model such as a Qwen3.6-35B-A3B or Qwen3-30B-A3B-style model when a stable quantized build is available.

The reason is simple: an MoE model may have a larger total parameter count, but only a smaller active subset is used per token. In practice, that can feel more responsive on limited hardware than forcing a full 27B-class model into 16GB.

For a 16GB M1 MacBook Pro, I would test models in this order:

1. Smaller Qwen models for daily speed
2. Qwen MoE/A3B-style quantized models for balance
3. Qwen3.6-27B 3-bit/IQ3 MLX for experimentation
4. Qwen3.6-27B 4-bit only if memory pressure stays acceptable
5. BF16 only on much larger-memory hardware

This is not about winning a model-size contest. It is about getting useful work done.

Practical engineer checklist

Before blaming the model, check the environment:

[ ] I am using Apple Silicon, not an Intel Mac.
[ ] I installed mlx-lm in a clean virtual environment.
[ ] I selected an MLX-compatible quantized model.
[ ] I avoided BF16 on 16GB memory.
[ ] I started with conservative sampling, such as temperature 0.1.
[ ] I used a short prompt.
[ ] I limited max tokens to 200–512.
[ ] I kept max-kv-size around 512–1024 for the first test.
[ ] I closed Docker, browsers, chat apps, and heavy IDE windows.
[ ] I watched Activity Monitor for memory pressure.

If all of that looks good and it is still too slow, the honest answer may be that the model is simply too large for comfortable daily use on this machine.

My recommended starting command

This is the command I would begin with:

mlx_lm.generate 
  --model <your-qwen3.6-27b-mlx-3bit-or-iq3-model> 
  --prompt "Explain the security risk of exposed Kubernetes dashboards in five practical points." 
  --max-tokens 256 
  --temp 0.1 
  --max-kv-size 1024

If that works, try a slightly larger output:

mlx_lm.generate 
  --model <your-qwen3.6-27b-mlx-3bit-or-iq3-model> 
  --prompt "Create a practical DevSecOps checklist for reviewing CI/CD secrets management." 
  --max-tokens 512 
  --temp 0.1 
  --max-kv-size 1024

If memory pressure stays green or low yellow, you can experiment. If it turns red, reduce the prompt size, reduce output length, lower KV cache, or switch to a smaller model.

What success looks like

A successful first test does not mean the model is fast. It means:

  • The model loads without the machine becoming unusable.
  • Memory pressure stays green or low yellow.
  • The prompt completes without heavy SSD swapping.
  • Short outputs are usable for engineering tasks.
  • Increasing output length does not immediately push the system into red memory pressure.

If those conditions are not met, do not keep tuning endlessly. Move to a smaller model or an MoE option. That is usually the better engineering decision.

Final takeaway

Qwen3.6-27B can be an interesting local experiment on a 16GB M1 MacBook Pro, but it is not the most comfortable daily-driver setup. Use MLX, choose an aggressive quant, start with conservative sampling, limit context, and watch memory pressure closely.

If you want the smoothest local engineering workflow, test a smaller Qwen model or a Qwen MoE/A3B-style model as well. The best local model is not always the largest one. It is the one that answers well enough without making your laptop crawl.

I Built a Full AI-Powered Production ready SaaS Platform Using MeDo from a Single Prompt #BuiltWithMeDo

Hackathons usually force developers into a familiar tradeoff: either spend most of the time wiring infrastructure together or cut corners just to get a working prototype shipped before the deadline. This time, I wanted to approach things differently. Instead of manually building everything from scratch, I wanted to test how far AI-native software development platforms have evolved.

That experiment led me to MeDo.

My goal was ambitious but straightforward. I wanted to build a fully functional SaaS platform called Coaching Moments, an AI-powered communication coaching application for corporate professionals. The platform would allow users to practice interviews, rehearse presentations, improve executive communication skills, and receive personalized AI feedback in real time.

What surprised me was not just that MeDo generated the application successfully, but how intelligently it approached the entire development process.

The Problem Behind Coaching Moments

Professionals spend years improving technical expertise, but communication often becomes the deciding factor in career growth. Whether someone is preparing for a leadership interview, presenting to executives, or handling a critical stakeholder meeting, confidence and communication quality matter enormously.

The problem is that most people do not have access to consistent coaching. Executive communication coaching can be expensive, difficult to schedule, and often inaccessible during the exact moments people need it most.

I wanted Coaching Moments to solve that problem by creating an AI-powered environment where professionals could safely practice high-stakes conversations. The idea was to make the experience feel less like a chatbot and more like a personal executive communication coach that is available anytime.

Users could simulate interviews, rehearse presentations, receive feedback on clarity and confidence, and track improvement over time. Beyond analytics and scoring, I also wanted the platform to provide motivational reinforcement because communication growth is deeply tied to confidence.

Why MeDo Felt Different Immediately

I have experimented with several AI coding assistants and low-code platforms before, but MeDo stood out almost immediately because it did not behave like a traditional code generator.

Most AI development tools start generating components and boilerplate code the moment you describe an idea. MeDo took a very different path. Instead of jumping directly into implementation, it first generated a structured requirements.md document based on my prompt.

That single step changed the entire experience.

Rather than treating my input as a simple coding task, MeDo treated it like a real product specification. It analyzed the business goals, the workflows, the user experience expectations, and the system architecture before generating the application itself.

It genuinely felt like collaborating with a product manager and engineering team rather than simply asking an AI assistant to write code.

Structuring the Prompt Like a Product Owner

One of the biggest lessons from this experience was that the quality of the outcome depended heavily on how the requirements were communicated.

I did not ask MeDo to “build an interview app.” Instead, I approached the prompt the same way I would communicate with an internal product and engineering organization.

I described the target users, the emotional experience I wanted users to feel, the dashboard architecture, the AI workflows, the onboarding process, and even the visual design language. I explained how the AI should behave during interview simulations, how presentation coaching should work, and what kind of feedback users should receive after each session.

To refine the prompt, I used ChatGPT as a product specification assistant. Together, the process became surprisingly powerful. ChatGPT helped structure the product vision clearly, and MeDo translated that specification into a working SaaS platform.

The final specification included authentication systems, AI-driven interview simulations, presentation rehearsal modules, analytics dashboards, subscription tiers, database models, responsive layouts, and AI integrations.

What impressed me most was how accurately MeDo interpreted the intent behind those requirements.

The Most Impressive Part of the Entire Experience

The most remarkable aspect of MeDo was the completeness of the generated application.

From essentially a single running prompt, MeDo generated authentication flows, backend APIs, database schemas, responsive dashboards, AI integrations, onboarding experiences, session management, and deployment-ready infrastructure.

The generated application already included modern SaaS expectations like dark mode support, polished onboarding flows, responsive layouts, and elegant dashboard components.

What genuinely surprised me was how stable the first generated version was. The prototype worked almost immediately and required very little debugging. Even the user experience felt polished from the beginning.

Instead of looking like a rough hackathon project, the application already resembled a production-ready SaaS platform.

Building the AI Coaching Experience

The feature I was most excited about was the AI coaching workflow itself.

Users could engage in interactive interview simulations where the AI dynamically asked follow-up questions based on their responses. They could also rehearse presentations and receive detailed feedback on speaking pace, clarity, confidence, and executive presence.

The feedback system was particularly impressive because it did not feel robotic. Instead of generic chatbot responses, the platform generated coaching-style insights that felt supportive and professional. It provided suggestions for improving communication structure, refining executive language, and strengthening storytelling during interviews and presentations.

In addition to critique, the platform also delivered motivational reinforcement. That emotional intelligence component made the experience feel much closer to real coaching than simple AI analysis.

Integrations and Full-Stack Automation

Normally, building a SaaS product with AI integrations requires significant effort across authentication systems, backend APIs, database configuration, frontend state management, and deployment infrastructure.

MeDo abstracted away much of that complexity.

The platform seamlessly handled frontend-backend connectivity, database structure generation, authentication flows, and AI integration scaffolding. Instead of spending hours configuring infrastructure and debugging setup issues, I was able to focus almost entirely on product thinking and user experience design.

That shift fundamentally changed how development felt.

Instead of operating primarily as an implementer, I found myself operating more like a product owner and systems designer.

Why This Experience Matters

What made this hackathon experience memorable was not simply the speed of development. AI tools have already demonstrated that they can accelerate coding workflows.

What felt different here was the transition from “AI-assisted coding” to “AI-assisted product engineering.”

MeDo understood workflows, architecture, requirements, and product intent in a way that felt much closer to collaborating with an engineering organization than using a traditional coding assistant.

The spec-driven development process was particularly valuable because it created alignment before implementation. That alone eliminated a significant amount of iteration and confusion.

For rapid SaaS prototyping, product experimentation, and early-stage startup development, this workflow feels incredibly promising.

Final Thoughts

Building Coaching Moments with MeDo genuinely changed my perspective on AI-native software development platforms.

The experience showed me that modern AI systems are evolving beyond isolated code generation and moving toward understanding entire product ecosystems. The ability to start with a product vision, review generated specifications, refine requirements, and receive a deployable SaaS application from that process is incredibly powerful.

Most importantly, it dramatically reduced the gap between idea and execution.

As someone who enjoys building products but often spends too much time managing infrastructure and setup, that shift felt significant.

This hackathon project started as an experiment to test AI-assisted development workflows. It ended up becoming one of the most productive and surprisingly polished software-building experiences I have had.

#BuiltWithMeDo

Why Your ‘Added to Cart’ Toast Notification Never Reaches Screen Reader Users (And the Two-Line Fix)

You add something to your cart on a Shopify store and a small message slides in from the top right of the screen. “Item added to cart.” It stays for three seconds, then fades away. The product page never reloaded. The cart icon updated a count. Everything feels smooth.

Now do the same thing with a screen reader running. The screen reader announces the button you clicked — “Add to cart, button” — and then nothing. No confirmation. No announcement that the item was added. No indication that anything happened at all. The cursor stays where it was. Three seconds later the visual confirmation has already disappeared from the screen, but the screen reader user never knew it existed.

This pattern is everywhere now. Cart confirmations. “Changes saved” banners in dashboards. “Message sent” notices on contact forms. Email signup confirmations. “Coupon applied” notifications at checkout. Login error messages that appear without a page reload. Almost every interactive surface on the modern web uses some version of a toast notification or snackbar to confirm that an action happened.

And almost every implementation of this pattern silently excludes screen reader users from confirmation.

This is the kind of accessibility failure that does not show up in a Lighthouse audit. It does not show up in axe DevTools. It does not show up when your developer manually reviews the page in Chrome. It only shows up when someone who actually uses a screen reader tries to use your site, gives up because nothing seems to be happening, and goes somewhere else. By that point you have already lost the sale and the customer has already added you to their internal “sites that do not work” list.

The good news is that fixing this is one of the smallest, cheapest, highest-impact accessibility improvements you can make. It is often two attributes on one HTML element. If you understand the pattern, you can usually fix it without rewriting anything.

What a toast notification actually is

A toast notification (named after toast popping out of a toaster) is a small message that appears temporarily, then disappears. Snackbar is the same concept from Google’s Material Design. Both are used for transient confirmations.

The visual pattern is consistent: a small box appears at the top, bottom, or corner of the screen, contains a short message, optionally has an icon, optionally has a dismiss button, and disappears on its own after a few seconds. The user does not have to click anything for the confirmation to go away.

The reason developers love this pattern is that it is non-blocking. Modal dialogs interrupt the user and require a click to dismiss. Inline error messages require the user to scroll to find them. Toast notifications appear, communicate, and leave without demanding anything. They feel polite.

But “non-blocking” is the same as “easy to miss.” For a sighted user who happened to glance at the corner of the screen, the toast registers. For a sighted user who was looking at the cart icon when the toast appeared in the corner, the toast does not register. For a screen reader user, the toast might as well not exist at all unless it has been built to announce itself.

The accessibility failure is not that the toast is temporary. It is that screen readers do not, by default, announce content that appears in the DOM after the page loads. The screen reader is busy reading whatever the user is currently focused on. Content that quietly appears off in the corner of the page does not interrupt the reading flow unless you specifically tell the screen reader that this new content is important enough to announce.

The two-line fix: aria-live

The W3C built an ARIA attribute specifically for this. It is called aria-live, and it tells assistive technology “when content changes inside this region, announce the new content to the user.”

There are two values that matter:

  • aria-live="polite" — wait until the user is between sentences or has paused, then announce the new content.
  • aria-live="assertive" — announce the new content immediately, interrupting whatever the user is currently hearing.

For most toast notifications, polite is the right choice. The user does not need to be interrupted mid-sentence to hear that their cart was updated. They will hear the announcement at the next natural pause. assertive is appropriate for genuine errors that the user needs to know about before they continue — for example, “Your session has expired and your changes were not saved.”

The fix looks like this:

<div aria-live="polite" id="toast-region">
  <!-- toast messages get inserted here -->
</div>

When your JavaScript updates the inner HTML of that div with the toast message, the screen reader will announce it.

That is the fix. Two attributes. One element. The visual styling of the toast does not change. The behavior for sighted users does not change. The only thing that changes is that screen reader users now hear what just happened.

The subtle part: where the region lives matters

The reason this trips people up is that aria-live regions are picky about timing. If you create the region and add content to it in the same JavaScript tick, screen readers sometimes miss the announcement. The region needs to already exist on the page when the new content is inserted.

The correct pattern is:

  1. On page load, create an empty live region in the DOM, typically at the bottom of the body or in a wrapper near the top.
  2. Style it however your toast notifications look — corner placement, drop shadow, fade animation, whatever.
  3. When you need to announce something, insert the message text into the existing region.
  4. After the toast disappears visually, remove the text from the region.

Here is a working HTML and JavaScript example:

<div aria-live="polite" id="toast-region" class="toast-region"></div>

<script>
  function showToast(message) {
    const region = document.getElementById('toast-region');
    region.textContent = message;
    setTimeout(() => {
      region.textContent = '';
    }, 4000);
  }

  // Example usage when the "Add to cart" button is clicked:
  document.querySelector('#add-to-cart').addEventListener('click', () => {
    // ...add the item to cart via your existing logic...
    showToast('Black wool scarf added to cart. Cart total: 3 items.');
  });
</script>

A few details that matter:

  • Use textContent, not innerHTML. Setting textContent triggers the live region announcement more reliably across screen readers.
  • Include enough context in the message that it makes sense out of context. “Item added to cart” is less useful to a blind user than “Black wool scarf added to cart. Cart total: 3 items.” The sighted user can see what they just clicked. The blind user is relying entirely on the message.
  • Do not put more than one live region with the same priority on a page. Multiple polite regions can cause announcements to step on each other. One region per page, multiple messages over time, is the cleanest pattern.

What about Shopify, WooCommerce, Squarespace, and Wix?

If you are not the developer of your own site, you may not be able to edit the HTML directly. Here is what works on each major platform.

Shopify. Most Shopify themes use the AJAX cart pattern and most of them ship without accessible toast notifications. The fix is in theme.liquid (or whatever your toast partial is called) — add an aria-live="polite" wrapper around the existing toast container. Shopify’s Dawn theme (the default since 2021) ships with this set correctly in newer versions, but custom and older themes frequently do not. If you cannot edit the theme files directly, this is a 15-minute job for a freelance Shopify developer. Worth doing before any audit.

WooCommerce. The default WooCommerce “Added to cart” message is a noticeable accessibility gap that has been an open issue on the project’s GitHub since 2019. Some themes patch it. Some do not. The fix lives in the theme’s single-product.php or loop/add-to-cart.php template, depending on which override is in place. For a non-developer, the most practical option is to install an accessibility-focused plugin that wraps the WooCommerce notice in an aria-live region.

Squarespace. Squarespace ecommerce uses a confirmation modal rather than a toast in most templates, which is a different accessibility pattern. The modal needs to manage focus correctly (moved into the modal on open, returned to the trigger on close). Squarespace’s default modal handles this reasonably well in newer templates, but custom code injection can break it. Test with VoiceOver before assuming it works.

Wix. Wix’s ecommerce confirmation is similar to Squarespace’s — a modal rather than a toast. Wix has improved accessibility on its Stores app in 2024–2026, but custom HTML embed widgets that you may have added do not get this treatment automatically. If you have a custom newsletter signup or contact form using Velo code, that is the place to check.

How to test in five minutes without coding

You do not need to know what aria-live is to test whether your toast notifications are accessible. Here is the five-minute test.

On a Mac, open System Settings, then Accessibility, then VoiceOver. Turn it on. (Cmd-F5 also toggles it.) Open your website. Add a product to cart, sign up for the newsletter, send a contact form message, or do whatever action your site uses toast notifications for. Listen.

If VoiceOver reads the toast message out loud, you are fine. If it stays silent, you have the failure described in this article and a 15-minute fix to schedule.

On Windows, the equivalent is NVDA, which is free. Same test: trigger the action, listen for the confirmation.

You do not need to be fluent with the screen reader to do this test. You are not navigating; you are just listening for whether the confirmation gets announced. If you can hear silence, you can run this test.

Why this matters more than it looks like it matters

It is tempting to dismiss this as a minor edge case. The toast appears, sighted users see it, blind users do not, but everything else on the site works, so what is the actual harm?

The actual harm is that the screen reader user does not know whether their action succeeded. They click Add to Cart. They do not hear anything. They cannot tell if the click registered. So they click again. Now they have two items in their cart, or three, or four, with no way to verify because the cart contents are also often delivered visually. They eventually navigate to the cart page to see what is there, discover the duplicates, and have to fix it manually.

Multiply that across every interaction on your site — every form submission, every save, every coupon application, every cart change — and you have a site that punishes screen reader users with constant uncertainty. They learn to mistrust the site. They go elsewhere. Their friends and family hear about the experience.

This is also the kind of failure that turns into ADA Title III demand letters. A plaintiff’s firm runs an automated scan, finds an “add to cart” flow with no accessible confirmation, sends a demand letter, and you are in settlement negotiations over what was a two-attribute fix. The cost of the demand letter and the legal response will exceed the cost of fixing the entire site, including the toast notifications, by an order of magnitude.

And finally: this is the most fixable kind of accessibility failure that exists. There is no design tradeoff. There is no performance cost. There is no need to involve UX research. You add an attribute. The site continues to look and behave exactly as before. The screen reader user gets the confirmation. Everyone wins.

A quick mental model for the next time you ship a notification

The next time your team is shipping any feature that quietly confirms something to the user — a save, an upload, a payment, a status change — ask one question: “If the user could not see the screen, how would they know this happened?”

If the answer is “they would hear the announcement from the live region,” you are doing it right. If the answer is “they would not,” you have an accessibility gap.

This question scales. It applies to “saved” banners, password-strength meters, character-count indicators, loading spinners that should announce when content is ready, validation errors that appear inline, search-result-count updates that change as filters are applied, and dozens of other patterns. Every one of them is a case where sighted users get visual feedback and screen reader users get silence — unless someone built the live region.

If you take one thing from this article, take this: silence is a bug. Every confirmation that exists for sighted users should exist for screen reader users too. Live regions are how you ship that.

Related Reading

We’re building a simple accessibility checker for non-developers — no DevTools, no jargon. Join our waitlist to get early access.

The One Rule I Follow to Code with AI (Without Losing My Skills)

I haven’t blindly jumped on the AI hype train. But I’m no hater either.

AI is here to stay. That’s why I’ve tried AI to offload tasks while coding.
But after a few weeks, I was so dependent I couldn’t write a simple LINQ query by hand.

So I decided to stop relying too much on AI and adopted this one rule:

Use AI outside my IDE or editor.

When I’m coding, I like to think I’m a surgeon in an operating room, and AI is my assistant doctor. They hand me tools and monitor my patient, but I’m always in charge.

How to be always in charge of AI

To stay in control, I never let AI into my editor. I use it in a browser tab.

It might seem slower or old-school. Sure!
But it forces me to decompose my problem and extract relevant code for AI.
And once I have an answer, it forces me to make it work on my side.
At least, I know if the output actually works.

Just like code you find on StackOverflow or anywhere else online, don’t use what AI gives you if you don’t understand what it’s doing.

Don’t let AI touch your code directly either.

AI is faster at generating code than us. No doubt! But being a good coder isn’t about typing fast. It’s about estimation, collaboration, and many more skills I’ve included in Street-Smart Coding—The roadmap I wish I had when I was starting out.

Playwright v1.60 Turns Test Failures Into Evidence

Playwright v1.60 makes failure evidence easier to capture during the run.

The main change is scoped HAR recording.

HAR means network request file.

It shows what the browser sent and received.

The release also adds file drops, ARIA boxes, and hard test aborts.

ARIA means accessibility map.

Together, these changes help CI failures explain themselves.

CI means automated build server.

The practical update

Use context.tracing.startHar() when network failures waste review time.

It records a HAR file inside Playwright tracing.

Tracing means run evidence capture.

Use locator.drop() when upload tests use custom events.

Drop API means file drop simulation.

Use page.ariaSnapshot({ boxes: true }) when AI tools inspect pages.

Boxes mean element positions.

Use test.abort() when shared setup finds unsafe state.

Fixtures mean shared test setup.

Code example

import { test, expect } from '@playwright/test';

test('upload records network evidence', async ({ context }) => {
  await using har = await context.tracing.startHar('upload.har', {
    content: 'embed',
    mode: 'minimal',
    urlFilter: //api/upload/,
  });

  const page = await context.newPage();
  await page.goto('/upload');

  await page.locator('#dropzone').drop({
    files: {
      name: 'note.txt',
      mimeType: 'text/plain',
      buffer: Buffer.from('hello'),
    },
  });

  await expect(page.getByText('Upload complete')).toBeVisible();
});

The HAR starts before the page opens.

The drop step sends an in-memory file.

When the test scope ends, Playwright finalizes the HAR.

The rule

Do not treat this release as a feature list.

Treat it as an evidence upgrade.

Better tests do not just pass or fail.

They explain what happened.

Read the canonical version:

https://www.anton.qa/blog/posts/playwright-v1-60-evidence-first-testing

Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at anton.qa or on LinkedIn.

Rasterization Using Bresenham Algorithm and Scanline Algorithm

1. Overview

  • Bresenham algorithm is the fastest algorithm for drawing straight lines on a pixel grid (using only integers).
  • Scanline Algorithm is commonly used to fill the interior of polygons by drawing horizontal scanlines between edges.
  • In this project, I implemented basic rasterization techniques in Python using Pygame by applying Bresenham Algorithm and the Scanline Algorithm.
  • The goal was to manually draw geometric shapes using lines and fill their interiors with color.
  • I learned how rendering and polygon filling work internally in graphics systems.

2. Development Process

core code

# draw line
def DrawLine(surface, x1, y1, x2, y2, color):
    dx = abs(x2 - x1)
    dy = abs(y2 - y1)

    stepX = 1 if x1 < x2 else -1
    stepY = 1 if y1 < y2 else -1

    error = dx - dy

    x = x1
    y = y1

    while True:
        surface.set_at((x, y), color)

        if x == x2 and y == y2:
            break

        e2 = 2 * error

        if e2 > -dy:
            error -= dy
            x += stepX

        if e2 < dx:
            error += dx
            y += stepY
  • This is the Bresenham Algorithm function.
  • Calculates difference between the x axis and the y axis (dx, dy) and uses the error value to determine at what point to move horizontally or vertically.
  • Efficient line rendering is possible because pixel positions are gradually updated through integer based error correction.
# fill triangle
def FillTriangle(surface, vertices, color):
    # sort vertices by y
    vertices.sort(key=lambda vertex: vertex[1])

    x1 = vertices[0][0]
    y1 = vertices[0][1]
    x2 = vertices[1][0]
    y2 = vertices[1][1]
    x3 = vertices[2][0]
    y3 = vertices[2][1]

    # interpolate x values between two points
    def Interpolate(yStart, xStart, yEnd, xEnd):

        if yStart == yEnd:
            return []

        xValues = []

        deltaX = (xEnd - xStart) / (yEnd - yStart)

        currentX = xStart

        y = yStart

        while y < yEnd:
            xValues.append(currentX)

            currentX += deltaX

            y += 1

        return xValues

    # create edge lists
    xShort = Interpolate(y1, x1, y2, x2)
    xShort += Interpolate(y2, x2, y3, x3)

    xLong = Interpolate(y1, x1, y3, x3)

    # get minimum length
    length = len(xShort)

    if len(xLong) < length:
        length = len(xLong)

    # fill triangle
    i = 0

    while i < length:

        y = y1 + i

        xa = int(xShort[i])
        xb = int(xLong[i])

        if xa > xb:
            temp = xa
            xa = xb
            xb = temp

        # draw horizontal line
        DrawLine(surface, xa, y, xb, y, color)

        # delay to visualization
        pygame.display.update()
        pygame.time.delay(15)

        i += 1
  • This function is the Scanline Algorithm.
  • First, align the vertices of the triangle with y axis, then use the Interpolate() function to interpolate the x coordinates of each side.
  • Afterwards, rendering is performed by calculating the left and right positions for each horizontal scanline and filling in the spaces between them.

# fill rectangle using 2 triangles!
def FillRectangle(surface, vertices, color):
    # 0 = top left
    # 1 = top right
    # 2 = bottom right
    # 3 = bottom left

    triangle1 = [
        vertices[0],
        vertices[1],
        vertices[2]
    ]

    triangle2 = [
        vertices[0],
        vertices[2],
        vertices[3]
    ]

    FillTriangle(surface, triangle1, color)
    FillTriangle(surface, triangle2, color)
  • This function fills a rectangle.
  • I implemented it by adapting the triangle filling function without the need for complex code.

3. Result

  • I implemented line and surface rendering myself.
  • When I thought about how I implemented the function to fill a rectangle using the function to fill a triangle, I felt like I had written efficient code.

Preview gif

Full Video

YouTube Video

Github Repository

Github – Graphics

Beyond Medical AI and AI Drug Discovery: Why Biomedicine Needs World Models

Over the past decade, AI in medicine has mostly moved through two major waves.

The first wave was medical AI.

It helped machines read, structure, summarize, and predict from medical data:

  • radiology image recognition
  • clinical note extraction
  • risk prediction
  • medical question answering
  • triage support
  • clinical documentation
  • patient record summarization

The second wave is AI drug discovery.

It tries to accelerate the discovery of intervention tools:

  • target discovery
  • molecular generation
  • protein structure prediction
  • virtual screening
  • toxicity prediction
  • preclinical optimization
  • drug-target interaction modeling

Both waves are important. Both will continue to matter.

But medicine has a deeper problem.

Medicine does not only ask:

What disease does this patient have?

Or:

Can we find a molecule that might affect this pathway?

Medicine ultimately asks:

If we intervene in this living system, will the patient’s trajectory actually change in a safe, explainable, and verifiable way?

That is the problem biomedical world models are beginning to address.

This article is a developer-oriented view of what a biomedical world model might be, why it matters, and why it must be built very differently from a chatbot or a standard prediction model.

Important disclaimer: biomedical world models are still an early research direction. The systems discussed here should be understood as hypothesis-generating, research-support, and decision-support frameworks — not validated clinical decision systems, not medical advice, and not substitutes for physicians, clinical trials, or regulatory review.

1. The missing layer in medical AI

Most current medical AI systems are good at perception, extraction, or prediction.

They answer questions like:


What does this image show?

What diagnosis is likely?

What is the patient’s risk?

What does this clinical note contain?

What happened in this hospitalization?

AI drug discovery answers a different question:


Can we find a molecule, target, protein interaction, or intervention candidate faster?

But there is a third question that is much harder:


If we apply this intervention to this patient or patient subgroup,

how might the biological and clinical trajectory change over time?

That is not just a prediction problem.

It is an intervention problem.

And intervention problems are harder because they require more than correlation. They require some combination of:

  • longitudinal patient state modeling
  • causal reasoning
  • counterfactual comparison
  • biological mechanism constraints
  • uncertainty estimation
  • feedback from real-world follow-up
  • auditability

A conventional risk model may say:


This patient has a high probability of disease progression.

A biomedical world model should help ask:


If we intervene now, does the trajectory change?

Compared with what counterfactual?

Through what mechanism?

With what uncertainty?

How will we verify it?

That is a very different engineering problem.

2. What is a biomedical world model?

In AI, a world model usually refers to a model that learns how an environment changes and uses that internal model for prediction, planning, or simulation.

In robotics, a world model may learn how an agent’s actions change the physical environment.

In games, it may learn how the game state evolves after each move.

In medicine, the analogy is tempting — but dangerous if taken too literally.

A human body is not a closed, fully observable, deterministic simulation environment. It is noisy, multi-scale, adaptive, heterogeneous, and only partially measured.

So a biomedical world model should not be understood as a complete digital human.

A safer definition is:

A biomedical world model is a computational framework that models how biological or clinical states may evolve over time under different interventions, constrained by data, mechanisms, counterfactual reasoning, uncertainty, and real-world feedback.

It is not simply a bigger medical LLM.

It is not just a risk calculator.

It is not a magic simulator of the entire body.

It is better understood as a framework for organizing:


state → intervention → trajectory → feedback → evidence

3. A minimal architecture

A biomedical world model needs more than one model.

A useful architecture may include at least seven layers.


[1] Patient / biological state representation

[2] Intervention representation

[3] Transition model

[4] Counterfactual engine

[5] Mechanism constraints

[6] Feedback and calibration loop

[7] Audit and uncertainty layer

Let’s unpack each layer.

4. Layer 1: State representation

The first problem is representing the current state.

In medicine, “state” is not just a diagnosis code.

A useful state representation may include:

  • clinical history
  • laboratory results
  • imaging features
  • medications
  • symptoms
  • genomics
  • transcriptomics
  • proteomics
  • metabolomics
  • methylation profiles
  • immune status
  • functional measurements
  • lifestyle variables
  • wearable data
  • longitudinal follow-up

In software terms, the model needs a structured representation of:


{

"patient_state": {

"clinical": {},

"molecular": {},

"functional": {},

"behavioral": {},

"temporal_history": {}

}

}

The hard part is that many of these fields are missing, noisy, biased, or measured using different platforms.

So state representation is not just an embedding problem. It is also a data quality, normalization, uncertainty, and clinical context problem.

5. Layer 2: Intervention representation

A world model must represent actions.

In medicine, actions are interventions.

Examples:


drug A

dose change

surgery

radiotherapy

nutrition plan

exercise intervention

sleep intervention

behavioral intervention

combination therapy

watchful waiting

A biomedical world model must distinguish between:


current state

intervention

post-intervention trajectory

Otherwise, it is only modeling natural history or observational risk.

A simplified schema might look like:


{

"intervention": {

"type": "drug",

"agent": "example_drug",

"dose": "low",

"duration": "12 weeks",

"target_pathway": "inflammation",

"expected_direction": "reduce inflammatory signaling"

}

}

But even this is simplified.

Real interventions involve adherence, timing, dose-response, patient heterogeneity, combination effects, and adverse events.

6. Layer 3: Transition modeling

The transition model asks:


Given state S and intervention A,

what future state S' might occur?

In notation:


S_t + A_t → S_t+1

For medicine, this is the core challenge.

The transition may involve:

  • short-term biomarker changes
  • delayed clinical outcomes
  • adverse effects
  • compensatory biological responses
  • organ-level changes
  • behavioral changes
  • disease progression or regression
  • treatment resistance

A naive predictive model might estimate:


P(outcome | current state)

A biomedical world model should move toward:


P(future state | current state, intervention, mechanism, context)

That is a much harder target.

7. Layer 4: Counterfactual reasoning

The most important clinical questions are often counterfactual:


What if we do nothing?

What if we intervene earlier?

What if we reduce the dose?

What if we choose another treatment?

What if the patient follows the plan only partially?

This is where standard machine learning can easily fail.

Observational medical data is full of confounding.

Patients who receive a treatment are not randomly selected. Physicians prescribe treatments based on severity, comorbidities, risk, access, and many unrecorded factors.

So a model trained only on historical associations may learn treatment patterns rather than treatment effects.

A biomedical world model therefore needs causal thinking.

It does not necessarily mean every system must implement a full formal causal graph. But it does mean developers must stop treating intervention modeling as ordinary next-token prediction or ordinary supervised learning.

At minimum, the system should ask:


What is the comparison?

What is the counterfactual?

What assumptions are being made?

What confounders may exist?

What evidence supports the causal claim?

8. Layer 5: Mechanism constraints

Medicine is not only data fitting.

A model that predicts something biologically impossible is not useful, even if it looks statistically plausible.

Biomedical world models need constraints from:

  • pathways
  • molecular mechanisms
  • pharmacology
  • physiology
  • clinical guidelines
  • disease natural history
  • known biomarkers
  • known safety boundaries

This does not mean we can manually encode all biology. We cannot.

But it does mean that mechanism-aware modeling is safer than purely black-box extrapolation, especially when the system is used to reason about interventions.

A world model should be able to explain:


Why is this intervention expected to change this state?

Which mechanism is assumed?

Which biomarkers should move first?

Which outcome would count as evidence?

What would falsify the hypothesis?

That last question is critical.

If a model cannot be wrong in any clear way, it cannot be scientifically useful.

9. Layer 6: Feedback and calibration

A biomedical world model should not stop after generating a prediction.

It needs a feedback loop.


prediction → follow-up measurement → comparison → calibration

For example:


The model expects inflammation markers to decrease after 8 weeks.

Follow-up data shows no change.

Now what?

Possible explanations:

  • the initial state was mischaracterized
  • the intervention was not followed
  • the assumed mechanism was wrong
  • the patient subgroup was wrong
  • the dosage was insufficient
  • the biomarker was not appropriate
  • the model extrapolated beyond its valid domain

This is where a world model becomes more than a prediction engine.

It becomes a framework for failure diagnosis.

That matters because in medicine, knowing why a model failed may be as important as knowing what it predicted.

10. Layer 7: Auditability and uncertainty

A biomedical world model should not produce false certainty.

It should express:

  • confidence
  • uncertainty
  • evidence strength
  • data limitations
  • assumptions
  • missing variables
  • out-of-distribution warnings
  • whether the output is hypothesis-level or decision-level

A useful system should distinguish between:


Supported by strong clinical evidence

Supported by mechanistic plausibility

Supported by weak observational data

Generated as a hypothesis only

Outside validated scope

This is especially important for medicine.

In consumer apps, a wrong recommendation may be annoying.

In healthcare, a wrong recommendation may harm a patient.

11. Why this matters for longevity medicine

Longevity medicine is a useful example because aging and chronic disease are trajectory problems.

Blood glucose, inflammation, immune function, muscle function, sleep, cognition, metabolism, and epigenetic markers all change over time.

A single test is only a snapshot.

The real question is:


Is the health trajectory improving?

For example, a biological age score may go down.

That sounds good.

But scientifically, it does not automatically mean:


better function

lower disease risk

longer lifespan

clinical benefit

A more rigorous model should ask:


Do multiple biomarkers move coherently?

Does function improve?

Does risk decrease?

Is the effect sustained?

Is there evidence of over-intervention?

Can follow-up validate the expected trajectory?

This is where weak biomedical world models may appear first.

Not complete simulations of the human body, but bounded systems that model specific aspects of health trajectories under defined interventions.

For example:


methylation + metabolism + functional measures + lifestyle + follow-up

Such systems should be used for research support or hypothesis generation, not as proof of clinical benefit.

12. Recent research signals

Some recent work points in this direction, although the field is still early.

One example is Medical World Model: Generative Simulation of Tumor Evolution for Treatment Planning, an arXiv preprint from 2025. It explores the idea of simulating tumor evolution after treatment decisions, moving beyond static image recognition toward post-treatment state simulation.

Another example is EHRWorld: A Patient-Centric Medical World Model for Long-Horizon Clinical Trajectories, an arXiv preprint from 2026. It explores long-horizon clinical trajectory modeling using electronic health records.

A related direction is SurvivEHR, a time-to-event foundation model for multiple long-term conditions from primary care electronic health records, published in npj Digital Medicine in 2026.

These works do not mean biomedical world models are clinically mature.

They mean the field is beginning to move from:


recognition and prediction

toward:


simulation, trajectory modeling, and intervention reasoning

That is a major shift.

13. Where SteeraMed fits

SteeraMed: A Steerable Biomedical World Model is a framework-level exploration of this direction.

Website:


https://SteeraMed.com

The core idea is that a biomedical world model should not only predict.

It should be steerable.

That means clinicians or researchers should be able to guide the model with meaningful directional inputs, such as:


reduce inflammation

improve metabolic resilience

avoid immune suppression

preserve muscle function

minimize adverse effects

But steerability does not mean arbitrary control.

It means the system should connect:


state representation

intervention direction

mechanism constraints

counterfactual reasoning

feedback calibration

auditability

A steerable biomedical world model should answer questions like:


What is the current state?

What intervention direction is being tested?

What mechanism is assumed?

What trajectory is expected?

What evidence would support it?

What evidence would falsify it?

Where is the uncertainty?

This is why steerability may be more important than raw prediction accuracy.

In medicine, the key question is not only:


Can the model predict the future?

It is:


Can we safely and transparently test whether an intervention changes the future?

14. What developers should not build

If you are building in this space, here are some traps to avoid.

Trap 1: A medical chatbot pretending to be a world model

A chatbot that explains diseases is not a world model.

A world model must represent state, intervention, time, feedback, and uncertainty.

Trap 2: A risk model pretending to simulate interventions

A risk model may predict probability under the status quo.

That does not mean it can estimate what happens after treatment.

Trap 3: A correlation engine pretending to be causal

Medical datasets contain confounding.

If your model cannot reason about counterfactuals, do not claim it estimates intervention effects.

Trap 4: A biomarker dashboard pretending to show health improvement

A biomarker change is not automatically a clinical benefit.

The model must connect biomarkers to function, symptoms, risk, or outcomes.

Trap 5: A black box pretending to be clinically trustworthy

In medicine, opacity is not a feature.

Auditability matters.

15. A developer checklist

If someone claims to be building a biomedical world model, ask:


1. What state does the model represent?
2. What interventions can it represent?
3. Does it model time explicitly?
4. What counterfactual is being compared?
5. What mechanisms constrain the prediction?
6. How is uncertainty expressed?
7. How is feedback incorporated?
8. What would falsify the model output?
9. What data domain is it validated on?
10. Is it a research tool, decision-support tool, or clinical product?

If these questions cannot be answered, it is probably not a biomedical world model.

It may still be useful.

But it should not be marketed as a world model.

16. Final thought

Medical AI helped us read medical data.

AI drug discovery helps us search for intervention tools.

Biomedical world models may help us ask a deeper question:


What happens after we intervene?

That question is central to medicine.

But it is also scientifically difficult.

Biomedical world models should therefore begin as cautious, auditable, weakly bounded systems for hypothesis generation and evidence organization — not as autonomous clinical decision machines.

The goal is not to replace doctors.

The goal is not to simulate the entire human body.

The goal is to build better computational frameworks for connecting:


state

intervention

mechanism

trajectory

counterfactual

feedback

evidence

If this direction matures, the next generation of biomedical AI may move beyond prediction and discovery toward something more important:

helping us test how to safely, explainably, and verifiably change health trajectories.

That is the real promise of biomedical world models.

References

  1. U.S. Food and Drug Administration. Considerations for the Use of the Plausible Mechanism Framework to Develop Individualized Therapies that Target Specific Genetic Conditions with Known Biological Cause. Draft Guidance, February 2026.

  2. U.S. Food and Drug Administration. FDA Launches Framework for Accelerating Development of Individualized Therapies for Ultra-Rare Diseases. FDA News Release, February 23, 2026.

  3. Prasad V, Makary MA. FDA’s New Plausible Mechanism Pathway. New England Journal of Medicine. 2025;393(23):2365–2367. doi:10.1056/NEJMsb2512695.

  4. Yang Y, Wang ZY, Liu Q, et al. Medical World Model: Generative Simulation of Tumor Evolution for Treatment Planning. arXiv:2506.02327, 2025.

  5. Mu L, Huang X, Gu Y, Qin S, Zhang S, Zhang X. EHRWorld: A Patient-Centric Medical World Model for Long-Horizon Clinical Trajectories. arXiv:2602.03569, 2026.

  6. Gadd C, Gokhale K, Acharya A, et al. SurvivEHR: a competing risks, time-to-event foundation model for multiple long-term conditions from primary care electronic health records. npj Digital Medicine. 2026. doi:10.1038/s41746-026-02709-z.

  7. Schölkopf B, Locatello F, Bauer S, Ke NR, Kalchbrenner N, Goyal A, Bengio Y. Toward Causal Representation Learning. Proceedings of the IEEE. 2021;109(5):612–634.

  8. Pearl J. Causality: Models, Reasoning, and Inference. Cambridge University Press.

  9. Hernán MA, Robins JM. Causal Inference: What If. Chapman & Hall/CRC.

  10. SteeraMed: A Steerable Biomedical World Model. https://SteeraMed.com

  11. Xiong J, et al. World Models for Biomedicine: A Steerability Framework. Preprints.org, 2026. doi:10.20944/preprints202605.0366.v1.