<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Coding with Matt]]></title><description><![CDATA[Diving into code and exploring languages, patterns, performance, and other aspects of making code better.]]></description><link>https://matthewtolman.com</link><image><url>https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png</url><title>Coding with Matt</title><link>https://matthewtolman.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 06 May 2026 04:18:56 GMT</lastBuildDate><atom:link href="https://matthewtolman.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Matt Tolman]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[mtolman@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[mtolman@substack.com]]></itunes:email><itunes:name><![CDATA[Matt Tolman]]></itunes:name></itunes:owner><itunes:author><![CDATA[Matt Tolman]]></itunes:author><googleplay:owner><![CDATA[mtolman@substack.com]]></googleplay:owner><googleplay:email><![CDATA[mtolman@substack.com]]></googleplay:email><googleplay:author><![CDATA[Matt Tolman]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Complexity Killed the Code]]></title><description><![CDATA[While everyone is rushing to generate as much code as possible, no one is stopping to ask what code should exist]]></description><link>https://matthewtolman.com/p/complexity-killed-the-code</link><guid isPermaLink="false">https://matthewtolman.com/p/complexity-killed-the-code</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Wed, 06 May 2026 01:19:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I switched jobs. I went from a very large company to a smaller company. I went from everything self-hosted and on-premises (source control, CI/CD, ticket tracking, service deployment, feature flag management, internal package mirror, etc.) to everything being hosted on a cloud SaaS provider. It is very painful. Not because of anything that my new company has done wrong (they&#8217;re fabulous), but because the service providers are doing a lot of things wrong.</p><p>So, I checked the service providers out. I saw what they&#8217;ve been up to. Every single one that has had reliability issues, without fail, is generating as much code as they can with AI. Every single one.</p><p>The stable SaaS companies? Very little to no mention of using AI. Something is rotting in the software world. I&#8217;ll leave it for you to find out, because that&#8217;s not what I&#8217;m interested in writing about.</p><p>What I am interested in, is the secondary trend that I found, and am experiencing, and that I&#8217;m seeing in tech. The reliable systems were a simple architecturally. Their blogs talked about monolithic architectures. Or they were in the very beginnings of micro services, so their service was still mostly monolithic. Many had self-hosting options. Monorepos were common. The SaaS companies that had the most issues were also overly complicated from a software technology perspective. Tons of talks of microservices, aggressive scaling, eventing, micro frontends, micro repositories, chained deployment pipelines, etc<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. Anyone who architected these software &#8220;solutions&#8221; would be unable to deploy on anything less than an entire data center, much less a handful of servers (or even one server). Doesn&#8217;t matter how big the servers are. These people have designed a system with so many pods, so many clusters, so many dependencies that they exceed the operational capacity of an entire rack in just container overhead.</p><p>The scary thing was that there was no sign of the complexity getting better, only of it getting worse. And the reliability is going with it.</p><p>And yet, I can&#8217;t help but feel like just saying &#8220;make things simple&#8221; won&#8217;t help. People justify complexity a lot. Sometimes too much. And sure, the naive solution won&#8217;t always scale. But it&#8217;s hard to understand simplicity when our textbook examples of how to do basic things are overly complex.</p><h2>The Textbook CRUD Application</h2><p>I&#8217;m going to start with a textbook CRUD application. Most SaaS code grows from these. I&#8217;m going to assume we&#8217;re using middleware for session management, authentication, and route authorization. That means when I show an endpoint, I&#8217;m not going to show the code loading the session from a datastore and verifying it (or getting it from a JWT, or whatever). I&#8217;m just going to show the CRUD endpoint. We&#8217;ll also be able to assume that whatever the CRUD endpoint does, the user has access to since if they don&#8217;t have access then they were filtered out before it reaches the code. Same thing goes for rate limiting, CSRF mitigations, CORS, database migrations, etc. Basically, anything not in the database communication flow is assumed to be taken care of elsewhere.</p><p>This series of assumptions is to only make the example more focused. We want to focus on a small part of the application - the textbook example - rather than the entire application. We can show how the rest of the application fares when applying the same techniques later on. For now, let&#8217;s just focus.</p><p>The textbook example we&#8217;re going to focus on is simply CRUD endpoints for a todo app. Yes, a todo app. The simplest, most plain, brain-dead, overdone code that anybody could copy-paste from millions of first-year college students and get something reasonable. We&#8217;re going to start there, since apparently simplicity in that type of application is hard to find.</p><p>Also, we&#8217;re only going to do an API for todo items. To help solidify things, here&#8217;s the schema we&#8217;re working with in a Postgres database.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;1a662100-8a67-4b94-943d-3fe9dd23f590&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE todo_items (
  id BIGSERIAL PRIMARY KEY NOT NULL,
  user_id BIGINT NOT NULL REFERENCES users(id),
  item_name TEXT NOT NULL,
  item_description TEXT NULL,
  done BOOLEAN NOT NULL DEFAULT false,
  deleted BOOLEAN NOT NULL DEFAULT false
);

-- session management and user data not shown to help us focus on todo items</code></pre></div><h3>The Textbook Method</h3><p>So, let&#8217;s define the textbook method. The &#8220;best practice&#8221; of ways to make our application. I&#8217;m not going to mirror an &#8220;ideal&#8221; MVC or MVVM or WYAI<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> method. I&#8217;m going to do things the Enterprise, Gold-Standard, In-Production Patent-Pending methodology I&#8217;ve seen in so many codebases with parallels advertised throughout my entire career (MVC and MVVM being some of those parallels - and yes, there really isn&#8217;t much difference that&#8217;s actually significant in anyway). I&#8217;m talking about classes.</p><p>In this model, we usually have a class representing the database table. We also have a class representing the API schema we&#8217;re exporting to. If we need to do any logic/processing/filtering on the server side, we&#8217;ll have a &#8220;service model&#8221; class to represent the todo item internally - that way we can process &#8220;ephemeral&#8221; todo items that may be from a database record, or an event queue, or an API request<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>.</p><p>So, let&#8217;s create our models. I&#8217;m going to do them in Java because it&#8217;s where this type of code is widely accepted, even though it sucks. Feel free to use AI to translate it to TypeScript or C# or PHP or Go or whatever. Though I doubt you&#8217;ll need to because most of the ceremony is utter nonsense anyways.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;6e52f17f-1c48-4d03-9029-632564c9ca0f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">package database.models;

@Entity
@Table(name = "todo_items")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitelyIncluded = true)
class TodoItem {
  @Id
  @GeneratedValue
  @EqualsAndHashCode.Include
  private long id;

  @Column(name = "user_id", referencedColumnName = "id")
  private long userId;

  @Column(name = "item_name")
  private String name;

  @Column(name = "item_description")
  private String description;

  @Column(name = "done")
  private boolean done;

  @Column(name = "deleted")
  private boolean deleted;
}

package api.models;

@Data
@Builder
class TodoItem {
  private String id;

  private String name;

  private String desc;

  private boolean done;

  private boolean del;
}

package service.models;

@Data
@Builder
class TodoItem {
  private long id;
  private long userId;
  private String description;
  private Boolean done;
  private Boolean deleted;
}

package utilities;

class TodoItemMapper {
  @Autowired
  private SessionManager sessionManager;

  public dbToService(db.models.TodoItem input) service.models.TodoItem {
    return service.models.TodoItem.builder()
      .id(input.getId())
      .userId(input.getUserId())
      .description(input.getDescription())
      .done(input.isDone())
      .deleted(input.isDeleted())
      .build();
  }

  public serviceToDb(service.models.TodoItem input) db.models.TodoItem {
    return db.models.TodoItem.builder()
      .id(input.getId())
      .user(userRepository.getById(input.getUserId()))
      .description(input.getDescription())
      .done(input.isDone())
      .deleted(input.isDeleted())
      .build();
  }

  public apiToService(api.models.TodoItem input) service.models.TodoItem {
    var builder = service.models.TodoItem.builder()
      .userId(sessionManager.getCurrentUser().getId())
      .description(input.getDesc())
      .done(input.isDone())
      .deleted(input.isDel());

    try {
      builder = builder.id(Long.parseLong(input.getId()));
    } catch (Exception e) {
      builder = builder.id(0);
    }

    return builder.build();
  }

  public serviceToApi(service.models.TodoItem input) api.models.TodoItem {
    return api.models.TodoItem.builder()
      .id(input.getId())
      .desc(input.getDescription())
      .done(input.isDone())
      .del(input.isDeleted())
      .build();
  }
}

package repositories;

@Repository
interface TodoItemRepository extends CrudRepository&lt;TodoItem, Long&gt; {
  TodoItem findByIdAndUserId(long id, long userId);

  List&lt;TodoItem&gt; findByUserId(long userId);
}

package api.routes;

import api.models.TodoItem;
// other imports excluded because they're self-obvious

@RestController
public class TodoItemController {
  @Autowired
  private TodoItemRepository todoItemRepository;

  @Autowired
  private SessionManager sessionManager;

  @Autowired
  private TodoItemMapper mapper;

  @GetMapping("/")
  public List&lt;TodoItem&gt; index() {
    return todoItemRepository.findByUserId(sessionManager.GetCurrentUser().GetId())
        .stream()
        .map(x -&gt; mapper.dbToService(x))
        .filter(x -&gt; !x.isDeleted())
        .map(x -&gt; mapper.serviceToApi(x))
        .collect(Collectors.toList());
  }

  @GetMapping("/{id}")
  public TodoItem getById(@PathVariable Long id) {
    var item = mapper.dbToService(
      todoItemRepository.findByIdAndUserId(sessionManager.GetCurrentUser().GetId())
    )
    if (item.IsDeleted()) {
      throw new ResourceNotFoundException();
    }
    return item;
  }

  // so many more methods, I don't feel like continuing this pain
}</code></pre></div><p>This is the &#8220;textbook,&#8221; &#8220;maintainable,&#8221; &#8220;easy&#8221; code. It&#8217;s so verbose. It&#8217;s so much code. There&#8217;s so much abstraction. There&#8217;s so much going on. And it&#8217;s slow. And it&#8217;s not maintainable. And it&#8217;s not easy to use. And it&#8217;s really just nonsense.</p><p>Almost all of that code is useless. And I&#8217;ll prove it to you.</p><h3>Simple Code</h3><p>Let&#8217;s replace all that code, with the following.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;1124857d-aac0-4a48-a634-2f436c61c4ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">
package api.routes;

import api.models.TodoItem;
// other imports excluded because they're self-obvious

@RestController
public class TodoItemController {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Autowired
  private SessionManager sessionManager;

  @GetMapping("/")
  public List&lt;Map&lt;String, Object&gt;&gt; index() {
    String sql = """
SELECT
  id AS "id"
  item_name AS "name",
  item_description AS "desc",
  done AS "done",
  deleted AS "del"
FROM todo_items
WHERE deleted = false AND user_id = ?
""";
   List&lt;Map&lt;String, Object&gt;&gt; data = jdbcTemplate.queryForList(sql, sessionManager.GetCurrentUser().GetId());
   return data;
  }

  @GetMapping("/{id}")
  public TodoItem getById(@PathVariable Long id) {
    String sql = """
SELECT
  id AS "id"
  item_name AS "name",
  item_description AS "desc",
  done AS "done",
  deleted AS "del"
FROM todo_items
WHERE deleted = false AND user_id = ? AND id = ?
LIMIT 1
""";
   Map&lt;String, Object&gt; data = jdbcTemplate.queryForMap(sql, sessionManager.GetCurrentUser().GetId());
   if (data == null || data.isEmpty()) {
      throw new ResourceNotFoundException();
   }
   return data;
  }
} </code></pre></div><p>So much simpler. So much less code. And so much more composable. For instance, let&#8217;s say we want to make sure the fields we select are standardized across our methods. We can easily do that with string concatenation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;6bdeb5ec-3b2e-4e77-b4a3-17f87294fc7e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@RestController
public class TodoItemController {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Autowired
  private SessionManager sessionManager;

  private static fields = """
  todo_items.id AS "id"
  todo_items.item_name AS "name",
  todo_items.item_description AS "desc",
  todo_items.done AS "done",
  todo_items.deleted AS "del"
"""

  @GetMapping("/")
  public List&lt;Map&lt;String, Object&gt;&gt; index() {
    String sql = "SELECT" + fields + "
FROM todo_items
WHERE deleted = false AND user_id = ?
""";
   List&lt;Map&lt;String, Object&gt;&gt; data = jdbcTemplate.queryForList(sql, sessionManager.GetCurrentUser().GetId());
   return data;
  }

  @GetMapping("/{id}")
  public TodoItem getById(@PathVariable Long id) {
    String sql = "SELECT" + fields + "
FROM todo_items
WHERE deleted = false AND user_id = ? AND id = ?
LIMIT 1
""";
   Map&lt;String, Object&gt; data = jdbcTemplate.queryForMap(sql, sessionManager.GetCurrentUser().GetId());
   if (data == null || data.isEmpty()) {
      throw new ResourceNotFoundException();
   }
   return data;
  }
}</code></pre></div><p>And just like that, we have one place to update our field selection for our class. Similar things could be done with other aspects, however it&#8217;s probably not necessary.</p><p>Now you might be thinking &#8220;well, that&#8217;s great if you are doing <em>basic</em> queries - but I need joins and nested data structures - no way that works!&#8221; Well, it does. You just haven&#8217;t learned SQL. Here&#8217;s one of my favorite SQL queries I&#8217;ve ever done, all in Postgres, and it handles formatting nested data, lists, etcetera. This was for quizes, but I love it.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;5228f456-2271-4d18-bb4b-e37cfa00a87f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT q.id as id,
        json_build_object('id', q.id, 'text', q.text) as question,
        json_build_object('id', m.id, 'type', m.type, 'url', m.url) as media,
        json_agg(json_build_object('id', a.id, 'text', a.text) ORDER BY a.pos ASC) as answers,
        ua."answerId" as "previousAnswer",
        ua."isCorrect" as "isCorrect",
        json_build_object('correctCount', uao."correctCount", 'totalAttempts', uao."totalAttempts") as summary
 FROM "quizSection" q
          INNER JOIN questions q ON q."questionId" = q.id
          LEFT JOIN media m ON q."mediaId" = m.id
          INNER JOIN answers a ON a."quizId" = q.id
          LEFT JOIN (
     SELECT ua."quizId", ua."userId", ua."answerId", a."isCorrect"
     FROM user_answers ua
              INNER JOIN answers a ON ua."answerId" = a.id
     WHERE ua."userId" = ?
 ) ua ON ua."quizId" = q.id
  INNER JOIN user_answer_overview uao ON q.id = uao."quizId" AND ua."userId" = uao."userId"
 group by q.id, q.id, m.id, ua."answerId", ua."isCorrect</code></pre></div><p>This handles grouping quiz sections with the question data, potential media information, answers (sorted in a specific display order), what the user&#8217;s previous answer was, if the user got the answer correct, and the user&#8217;s total attempts (and number of correct attempts). It formats all of this as nested objects that I could simply JSON serialize and return to the user. It&#8217;s all in the format my API desired.</p><p>One query to aggregate a lot of data from different tables, and then simply serialize and return it. No mess of database models, service models, and API models. A single input parameter for the current user id. My server code was literally just the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">async (req: Request, res: Response) =&gt; {
   res.send(await db.query(`SELECT q.id as id,
        json_build_object('id', q.id, 'text', q.text) as question,
        json_build_object('id', m.id, 'type', m.type, 'url', m.url) as media,
        json_agg(json_build_object('id', a.id, 'text', a.text) ORDER BY a.pos ASC) as answers,
        ua."answerId" as "previousAnswer",
        ua."isCorrect" as "isCorrect",
        json_build_object('correctCount', uao."correctCount", 'totalAttempts', uao."totalAttempts") as summary
 FROM "quizSection" q
          INNER JOIN questions q ON q."questionId" = q.id
          LEFT JOIN media m ON q."mediaId" = m.id
          INNER JOIN answers a ON a."quizId" = q.id
          LEFT JOIN (
     SELECT ua."quizId", ua."userId", ua."answerId", a."isCorrect"
     FROM user_answers ua
              INNER JOIN answers a ON ua."answerId" = a.id
     WHERE ua."userId" = ?
 ) ua ON ua."quizId" = q.id
  INNER JOIN user_answer_overview uao ON q.id = uao."quizId" AND ua."userId" = uao."userId"
 group by q.id, q.id, m.id, ua."answerId", ua."isCorrect`, req.session.user))
}</code></pre></div><p>Could things be simpler? Absolutely! The database model was definitely a mess. Having a view built-in to the database, less normalization (like why is quiz section and question data separate when there is a one-to-one mapping? why is media and question data separate when there is at most a one-to-one mapping?), and more would help. Ideally the query from the server would simply become the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;868c6b6a-c58f-449b-b74d-e77e331347ea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">async (req: Request, res: Response) =&gt; {
   res.send(await db.query(`SELECT * FROM quiz_view WHERE user_id = ?`, req.session.user))
}</code></pre></div><p>Then, all the complexity above would be handled in the database layer with the migrations. I could then do things like use <a href="https://www.postgresql.org/docs/current/rules-materializedviews.html">materialized views</a> to cache read-heavy workloads, add additional indexes based on the query plan, and even modify the table structure without breaking application code (the view just has to map). There wouldn&#8217;t be this nonsense of having a data object match the table structure just so we then map it to a service model so we can operate on it in-memory and then map it to an API model so we can remove the data that we don&#8217;t show the user.</p><p>Which also brings up another point of mine.</p><h3>To the nay-sayers</h3><p>Some people are going to argue that the SQL code above is not really all that simple, or that it&#8217;s hard to read, or debug, or whatever. Except it&#8217;s not. Compare the SQL with the &#8220;much simpler&#8221; Repository code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;8343f62a-a2c5-4fae-98b9-b4b8a40b4126&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@Repository
interface QuizeRepository extends CrudRepository&lt;Quiz, Long&gt; {
  List&lt;Quiz&gt; findQuizByUserId(long userId);
}</code></pre></div><p>Now, tell me what those above functions do <em>exactly</em>. Tell me what the SQL plan is. Tell me if there&#8217;s a scan or an index lookup. Tell me what joins happen, and when. Tell me how many network trips happen. Tell me when those network trips happen. Is it when I access a field, or sub-field, or a list element? Is it all at once even or is it lazy? Tell me when the framework switches between lazy and eager. Tell me how to debug it when it&#8217;s getting the wrong fields, or not enough fields, or making too many requests. Tell me what happens if I lose my database connection. Tell me what happens if I lose my database connection after I accessed some fields, but not others. Tell me how it runs on the database. Show me the execution plan. Tell me how it operates under load. Tell me how to make it faster. If you can&#8217;t answer all of those from that snippet and a connection to your production database, then your solution is objectively worse than mine.</p><p>With my snippet you can get all of that information and more. You can even get the current worse case scenario and best case scenario. You can see every index used and not used. You can control how much is loaded and when. You can add in materialized views with ease.</p><p>At the end of the day, for both code snippets to be functionally equivalent, the SQL in the second example must be <em>at least</em> as complex as what I showed earlier. You just can&#8217;t see it. Instead, you see mindless classes, that offer no real value outside of extra memory allocations and memory copies for your garbage collector to clean up. You see annotations that are &#8220;magic&#8221; without thinking about the runtime cost (hint: most of those annotations use reflection - one of the slowest operations in any runtime/interpreter). You don&#8217;t see SQL, you see class method accesses. You don't even see your class data members, just getters and setters. Meanwhile that SQL - the thing your database runs and is the slowest part of your request - is generated on the fly, at runtime, and ran against your database without any sort of review at all. No DBA looked at it, no team member gave it a glance, and you did not even check if it&#8217;s using an index or not. You just shipped it.</p><p>At the end of the day, your users suffer because your query that you didn&#8217;t write - only generated - was slow. Your server suffers because you filled it with garbage memory and wasted millions of cycles per request copying data from class to class to class to class. Your garbage collector suffers from frequent, short-lived memory allocations blocking it from cleaning up the real stuff taking up memory (that&#8217;s how generational garbage collectors work - short-lived allocations block it from cleaning up long-lived allocations). Your database suffers because you&#8217;re hitting it with thousands of unvetted, unoptomized queries that you don&#8217;t even know what they do or where they came from or why sometimes they get everything at once and why sometimes they make hundreds of micro-queries per field.</p><p>All of this in the name of &#8220;abstraction.&#8221; Your abstraction brought in complexity, it killed your code, and AI cannot save you since it&#8217;s been trained to increase the poison - not lower it.</p><p>In the best case, you have DBAs pinging you at 2:00 in the morning asking what code is running such-and-such query that&#8217;s killing the database and you don&#8217;t know because you never wrote the query - and you have to find out. In the worst case, that urgent message is never sent because nobody cares, and your application joins the rest of the rot. Reliability goes from 99.99% to 99.9% to 99%, 98%, 95%, 92%, 90%, 85%, 70%, and down and down and down. Users start questioning your company. They look for alternatives. <a href="https://mitchellh.com/writing/ghostty-leaving-github">Some are forced off even though they once loved your product</a>.</p><p>And so, the product dies. Slowly. Very slowly. Until something both breaks and a competitor gains advantage. Then, irrelevancy. Just like MySpace, and Yahoo Search. Maybe a rebrand happens, like Hotmail to LiveMail. Or maybe an aquisition like Sun Microsystems. Or just bankruptcy like Palm.</p><p>Of course, that may not matter much for you. There&#8217;s always another dying software that needs more poison, more bad abstractions, and more cycles wasted in the name of productivity and best practice. If, after all of this, you&#8217;re still fine with the complexity of the common abstractions, then go ahead. Use your AI tools as long as they&#8217;re affordable. Go nuts. I can&#8217;t convince you, so this post isn&#8217;t for you. If, on the other hand, you&#8217;re starting to question the textbook, then keep questioning it. Question the &#8220;best practice&#8221; because, at the end of the day, it&#8217;s almost all a cargo cult. Built to hide, not know.</p><h2>Back to Microservices, AI, and SaaS</h2><p>We just looked at a very basic &#8220;best practice&#8221; that kills code. Now, stack that on top of itself. We have controllers, views, models, and database models. Add in eventing with their own models, controllers, and tie those into your existing models and databases. Add in services with API requests. Now wrap those requests in a cache layer, wrap those cache layers in models, and integrate those models with your existing models and services.</p><p>Now do that again. And again. And again. And you have a little series of services that does one little part of a feature. Now keep doing it several dozen more times. Now you have most of a feature. Do it several dozen more times, and now you have a button that can send an email some time in the future for an authenticated user. Keep going. Now you can send emails immediately. Keep going. Do it a few thousand times. Maybe a few million times. Eventually, doing it enough times, you get GMail. Or maybe GitHub. Or NPM. Or any other SaaS<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>.</p><p>And yet, they didn&#8217;t need the microservices. They didn&#8217;t need the ORM, or the many model layers. Many of them don&#8217;t even need the eventing (or at least, not &#8220;proper&#8221; eventing - a database table with a status column and a single background job often suffices).</p><p>Even at scale, many companies don&#8217;t need microservices. It actually makes scaling harder. Now to scale up one service, you need to scale up everything that it touches - whether that&#8217;s event pipelines or direct API calls anybody that it puts load on needs to scale. But, to scale those services, you need to scale everything they touch. And so on.</p><p>And, if you ever want to do a sweeping change - like update from Java 8 to Java 11 - you now need to do that work thousands to millions of times. Every service needs to be updated, tested, fixed, and tested again.</p><p>Now do that for your frontend too. Do that with Angular. Or React. Try to keep your Node versions updated. Upgrade your build pipelines so you can upgrade your Node. Do that thousands of times.</p><p>And keep the network stable. Make sure you don&#8217;t overload the network. Make sure that you don&#8217;t have too much latency. Make sure you don&#8217;t route too much traffic from thousands of services to a single server.</p><p>And now debug things when they go wrong. Try to figure out what actually happened when a user clicked a button. Which services were called, when were they called, how were they called (API? gRPC? event?). Which ones failed. Which ones tried to fix the failure and made it worse. Which API gateway decided to drop the message before it hit the service - and why.</p><p>Now handle incidents. Try to get the right teams on an incident call. I&#8217;m sure they all kept your wiki updated - especially after that reorg last month. Try to keep track of which services are unreliable. Track the cascading failure. Which ones take down your site? What unexpected parts went down?</p><p>Now do this across time zones. Have some developers in Central India and some in on the US East Coast. They&#8217;ll never meet, or talk. Except maybe in an incident. How do they respond, under pressure, with complete strangers, in the middle of the night, after a long day of work, while the other people are about to go to lunch, and when the optics are bad?</p><p>Now add in some acquisitions. Little bubbles with completely different technologies, languages, patterns, design, and philosophy. Now apply pressure for them to conform or leave.</p><p>And finally, add in AI. Something designed to take all of the data of every instance of any of the above situations and amplify it. Automate it. But, do it in a way that is scientifically shown to be the most enticing and pleasing.</p><p>What do you expect the outcome to be? I expect it to mirror what we&#8217;re already seeing. Increased outages, especially from high profile companies. Increased severity of outages when they happen. Decreased public satisfaction with software. Increased concern about relying on SaaS. In short, I expect our complexity to kill our code.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>It&#8217;s also interesting how the companies with the most complex code are the ones relying on AI the most, while the companies with less complex code don&#8217;t rely on AI as much. It&#8217;s almost like building a system that no person could ever hope to understand breeds a helplessness that heavy AI usage can prey on, while building a system that&#8217;s comprehensible builds a sort of resistance to AI.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Whatever-Your-Acronym-Is</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>And yes, this is very much real. I have worked on many production code bases with this as the simplest case. Usually it was a lot worse.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Almost all of them send emails now or in the future.</p></div></div>]]></content:encoded></item><item><title><![CDATA[A Library for JavaScript Threads]]></title><description><![CDATA[Complete with mutexes and other SharedArrayBuffer primitives]]></description><link>https://matthewtolman.com/p/a-library-for-javascript-threads</link><guid isPermaLink="false">https://matthewtolman.com/p/a-library-for-javascript-threads</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Sat, 28 Mar 2026 18:50:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I wrote quite a bit about <a href="https://matthewtolman.com/p/playing-with-threads-in-javascript">threads in JavaScript</a>, <a href="https://matthewtolman.com/p/sharing-memory-across-threads-in">sharing memory</a>, <a href="https://matthewtolman.com/p/better-mutexes-in-javascript">building mutexes</a> (and <a href="https://matthewtolman.com/p/javascript-condition-variables">other synchronization primitives</a>), etc. It&#8217;s a lot of details, and it was really fun to write about. But, I had a problem. I didn&#8217;t have a way for developers to easily use the content without running into weird, undocumented, technical issues - and that&#8217;s kind of a problem.</p><p>It turns out, threads aren&#8217;t that used, and quite a bit of information I encountered was outdated or flat-out wrong (the highest ranking article on how to use threads claimed that only strings could be passed as messages - something that is absolutely untrue). So,  there was a lot of trial and error in just getting something basic for normal workers.</p><p>Since it&#8217;s such a problem, I decided to make a library (which I&#8217;ll cover in a bit). That library represents a culmination of trial and error, learning, and testing. Is it finished? Not yet. Mostly I&#8217;m reworking the algorithm for handling crashed threads in a thread pool. But, it&#8217;s good enough for a pre-1.0 release.</p><h2>Show Me the Code!</h2><p>First, let&#8217;s install it with NPM.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;9987c6b4-e9f8-4d72-a901-83119928b946&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">npm i -S peak-threads</code></pre></div><p>Now let&#8217;s do something simple. Send work, receive a response. No shared memory. Here&#8217;s the example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;20daa2ff-f102-4dfe-9913-d72024e72cc0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// main.js
import {Thread} from "peak-threads"

async function simpleExample() {
  const thread = await Thread.spawn('worker.js', {type: 'module'})
  return await thread.sendWork({op: 'add', inputs: [2, 5]})
}

// worker.js
import "peak-threads"

onwork = ({op, inputs}) =&gt; {
  switch (op) {
    case 'add': return inputs.reduce((a, b) =&gt; a + b)
  }
}</code></pre></div><p>The above code is fairly straightforward. In the main thread, you simply import the library, spawn a thread<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, wait for it to initialize, and then send it work. In the worker thread, you simply register a callback handler for when work comes, and you return the result. No need to manage listening or anything.</p><p>Of course, since JavaScript has many ways of doing things, and since TypeScript doesn&#8217;t like global unknown handlers like the above. So, I have provided a function wrapper for registering callbacks that can be used instead. Example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;b2ebbcc2-8782-4c96-b2f7-5c3c5219db6f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">import {registerHandler} from 'peak-threads'

registerHandler('work', ({op, inputs}) =&gt; {
  switch (op) {
    case 'add': return inputs.reduce((a, b) =&gt; a + b)
  }
})</code></pre></div><p>Practically the same code, just a little different dressing and more compatible with TypeScript.</p><p>We can also avoid copies for large data objects (e.g. image data) by &#8220;transferring&#8221; data over. For example, here we send an array buffer over:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;48b154a3-a91e-4831-9440-fe40e0719c5e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// main.js
import {Thread} from &#8220;peak-threads&#8221;

async function simpleExample() {
  const data = new ArrayBuffer(1024 * 1024 * 5)
  const array = new Int8Array(data)
  const thread = await Thread.spawn(&#8217;worker.js&#8217;, {type: &#8216;module&#8217;, closeWhenIdle: 100})
  return await thread.sendWork(array, {transfer: array.buffer})
}

// worker.js
onwork = (arr) =&gt; {
  // do some work
  arr.set([1, 2, 3], 0)
  // transfer the memory back
  return ResponseWithTransfer(arr, [arr.buffer])
}</code></pre></div><h3>Managing Threads</h3><p>The above examples do leak resources since we don&#8217;t close the thread. We can fix that with either a call to &#8220;close&#8221; or a &#8220;closeWhenIdle&#8221; parameter to our thread spawn function.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;fbda0a12-5661-4dfc-b93d-1bccbef4442e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// main.js
import {Thread} from &#8220;peak-threads&#8221;

async function simpleExample() {
  // close when idle for 100ms
  const thread = await Thread.spawn(&#8217;worker.js&#8217;, {type: &#8216;module&#8217;, closeWhenIdle: 100})
  try {
    return await thread.sendWork({op: &#8216;add&#8217;, inputs: [2, 5]})
  }
  finally {
    // immediately close
    thread.close()
  }
}</code></pre></div><p>Better yet, let&#8217;s simply create a global thread pool to manage threads for us. We can even make the pool global so our whole app can use it!</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;dc175735-859c-4e83-a74f-6f850ee1341f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// main.js
import {ThreadPool} from &#8220;peak-threads&#8221;

let getPool = ThreadPool.spawn(&#8217;worker.js&#8217;, {type: &#8216;module&#8217;}) // returns a promise

async function simpleExample() {
  const pool = await getPool // since this is a promise, we need to await
  return await pool.sendWork({op: &#8216;add&#8217;, inputs: [2, 5]})
}</code></pre></div><p>The thread pool will now handle managing the threads for us!</p><h3>React and Vite</h3><p>If we&#8217;re in React (or another framework with contexts or async state), we could simply wrap our pool and not render any components that rely on the pool until it&#8217;s ready. If we&#8217;re using Vite, it might look something like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;baa4b3bc-624b-4bcf-a7f6-3275a491fd33&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">import {ThreadPool} from "peak-threads";
import {createContext} from "react";
import React, {useEffect, useState} from "react";

// Tell Vite to compile a separate worker entry point
// We'll then pass this URL to our thread pool
import WorkerUrl from "./worker.ts?worker&amp;url";

export const PoolContext = createContext((null as any) as ThreadPool);

export function ReactThreadPool({children}: any) {
    const [pool, setPool] = useState&lt;Pool&gt;(undefined as any)

    useEffect(() =&gt; {
        Pool.spawn(WorkerUrl, {type: 'module'}).then(p =&gt; setPool(p))
    }, [])

    return (
        &lt;PoolContext value={pool}&gt;
            {pool ? children : &lt;&gt;&lt;div&gt;Initializing...&lt;/div&gt;&lt;/&gt;}
        &lt;/PoolContext&gt;
    )
}

// app.tsx
function App() {
  return (
    &lt;ReactThreadPool&gt;
      &lt;MyComponentThatUsesPools /&gt;
    &lt;/ReactThreadPool&gt;
  )
}

// my-component-that-uses-pools.tsx
export function MyComponentThatUsesPools() {
    const [result, setResult] = useState(0)
    const [running, setRunning] = useState(false)
    const pool = useContext(PoolContext)

    return &lt;&gt;
        &lt;button
            disabled={running}
            onClick={
                async () =&gt; {
                    setRunning(true)
                    setResult(await pool.sendWork({type: 'expensive-calculation'}))
                    setRunning(false)
                }
            }
        &gt;
            Run Calculation
        &lt;/button&gt;
        &lt;p&gt;
            {result}
        &lt;/p&gt;
    &lt;/&gt;
}</code></pre></div><p>A little verbose, but not too bad. Most of it is &#8220;one-time&#8221; boiler plate that can easily be wrapped in a library. Also, it hides the &#8220;getting a pool is async&#8221; issue, so all of our threading code can assume a pool is ready.</p><p>Of course, most existing libraries out there already do stuff like this just fine. They can handle wrapping workers, or sending threads, or correlating messages just fine. So, let&#8217;s take it up a notch. Let&#8217;s go where other libraries don&#8217;t (or at least, not yet).</p><h2>The Cool Code</h2><p>First, while most libraries let you wrap workers in another class, they don&#8217;t let you <em>send</em> or <em>receive</em> a class. I do. Here&#8217;s the code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;ed689eb5-bbf3-4186-b458-d31399ec116c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// my-typescript-class.ts
import {registerDeHydration} from 'peak-threads'

class MyTypeScriptClass {
  private a: number
  private b: number

  constructor(a: number, b: number) {
    this.a = a
    this.b = b
  }

  public function sum() {
    return a + b
  }

  //// Hydrate/Dehydrate Methods used for sending a class
  //// This is where the magic happens

  static hydrate({a, b}: {a: number, b: number}) {
    return new MyTypeScriptClass(a, b)
  }

  static dehydrate(instance: MyTypeScriptClass) {
    return {a: instance.a, b: instance.b}
  }
}

// magic line that makes the above hydrate/dehydrate methods work
registerDeHydration({key: 'MyTypeScriptClass', type: MyTypeScriptClass})

// main.ts
import {MyTypeScriptClass} from './my-typescript-class.ts'
import {getPool} from './get-pool' // &lt;- sets up pool like we showed above

async function doSum(a: number, b: number) {
  const pool = await getPool
  const c = new MyTypeScriptClass(a, b)
  return pool.sendWork(c)
}

// worker.ts
import {registerHandler} from 'peak-threads'
import {MyTypeScriptClass} from './my-typescript-class.ts'

registerHandler('work', (c: MyTypeScriptClass) =&gt; c.sum())</code></pre></div><p>Here, we &#8220;send&#8221; a class and we &#8220;receive&#8221; a class. Really, behind the scenes we&#8217;re calling the dehydrate method to get a transferable object, tagging the object  with the key string from the register call, transferring it, and reversing the process on the other side (using the tag to know what hydrate method to call). It&#8217;s really just automatic serialization and deserialization. But, it works really well.</p><p>We can also use this coolness combined with initialization data. When a thread is spawned, we can set an option &#8220;initData&#8221; with our initial data to send to the thread. The thread is guaranteed to receive that data before the promise returns - so we know that everything is all setup and ready. Our thread registers an &#8220;oninit&#8221; handler which receives the initial data - that way we can save it or process it however we need to. Here&#8217;s an example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;c762300b-f6d2-4b55-9db3-14f373d2b82e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// main.ts
import {MyTypeScriptClass} from './my-typescript-class.ts'
import {Thread} from 'peak-threads'

async function doSum(a: number, b: number) {
  const c = new MyTypeScriptClass(a, b)
  const thread = Thread.spawn('worker.js', {initData: c})
  return thread.sendWork()
}

// worker.ts
import {registerHandler} from 'peak-threads'
import {MyTypeScriptClass} from './my-typescript-class.ts'

let c: MyTypeScriptClass

registerHandler('init', (i: MyTypeScriptClass) =&gt; c = i)
registerHandler('work', () =&gt; c.sum())</code></pre></div><p>I use the serialization and initial data for transferring objects with SharedArrayBuffers provided by the library - such as mutexes! Here&#8217;s an example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;1174fe7f-d6fc-483d-ab85-b6158111b355&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// main.js
const {Mutex, Thread} = import("peak-threads")

async function sharedMemExample() {
    const mem = new Int32Array(new SharedArrayBuffer(64))
    const mux = Mutex.make()
    const [thread1, thread2] = await Promise.all([
      // Initialize our thread with shared memory and a mutex
      Thread.spawn('worker.js', {initData: {mem, mux}}),
      Thread.spawn('worker.js', {initData: {mem, mux}})
    ])

    // lock the mutex, write to memory, and queue work
    await mux.lockAsync()
    mem.set([1, 2, 3], 0)
    
    const promise = Promise.all([
      thread1.sendWork({add: {v: 10, i: 0}}),
      thread2.sendWork({add: {v: 20, i: 2}}),
    ])
    // unlock to let them run
    mux.unlock()

    // wait for the results
    const [r1, r2] = await promise
    
    // Prints: 11, 23
    console.log(r1, r2)
}


// worker.js
let memory, mutex
oninit = ({mem, mux}) =&gt; {
    // Save our initial data
    memory = mem
    mutex = mux
}

onwork = ({add}) =&gt; {
    // lock our memory
    mux.lock()
    try {
        // Read from our memory, do some math, return
        // returned data is automatically sent back to the caller
        return add.v + memory.at(add.i)
    }
    finally {
        mux.unlock()
    }
}</code></pre></div><p>The code above demonstrates how we can share memory between threads in a fairly straightforward manner.</p><p>Some might be wondering, well why do we need to pass the mutex in the initial data? The answer is, there&#8217;s a hidden race condition where sometimes when shared array buffers are being transferred to one thread (Thread A) while another thread (Thread B) is writing, then Thread A may reset the shared buffer and lose Thread B&#8217;s data. I am not at all sure why this happens. I&#8217;ve been debugging it for a very long time. I get magical &#8220;resets&#8221; that don&#8217;t happen from my code, just the browser. I&#8217;ve been able to reproduce it quite reliably - though it only happens about 1 out of every 10,000 runs. The best workaround I&#8217;ve found is that I just need to wait for shared memory to &#8220;settle&#8221; before I start using them. That&#8217;s way I have initial data and an asynchronous spawn - it&#8217;s so that shared memory can &#8220;settle&#8221; before it&#8217;s used.</p><h2>Other Features</h2><p>My library has other features in it as well, such as sending messages without waiting for responses (called &#8220;events&#8221; - can be sent/received from both sides), barriers, condition variables, wait groups, semaphores, etc. Also, I have optional debug logging that you can turn on with &#8220;setLogging&#8221;. This will print a lot of debug messages whenever events are sent or transformed, and it will print the thread id that it&#8217;s tied to (the thread ids also show the parent thread chain, so you can see &#8220;oh, this is a child of a child thread&#8221; which I have found helps). If you want to get a thread&#8217;s id, simply use &#8220;curThread()&#8221;. Do note that &#8220;setLogging&#8221; only turns on logs for <em>that thread</em>. It&#8217;s not a &#8220;global&#8221; logging setter. I did that so you can focus on debugging specific threads and not get a bunch of background worker noise (e.g. from another thread pool).</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;10dd8cf5-9f6d-4c3a-b4ca-65fccb86b912&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">import {curThread, setLogging} from 'peak-threads'

// This works from any thread - including the main thread!
setLogging(true) // turn debug logs on
setLogging(false) // turn debug logs off - default
console.log(curThread()) // prints the current thread id</code></pre></div><p>On other important note is that I have overridden the &#8220;postMessage&#8221; and &#8220;onmessage&#8221; handlers for workers (and Worker objects). From what I can tell, most libraries do this to some extent as there really isn&#8217;t a lot you can do without overloading or wrapping it in some way. I just overloaded it instead of wrapping it. This means if you call &#8220;postMessage&#8221; you&#8217;ll get the automatic class sending, debug logging, etc. (but it will be triggered as an &#8220;event&#8221; not &#8220;work response&#8221; or &#8220;work request&#8221;). </p><h2>Links</h2><p>The source code can be found on <a href="https://github.com/matthewtolman/peak-threads">GitHub</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. I also have published it to NPM under the name &#8220;<a href="https://www.npmjs.com/package/peak-threads?activeTab=readme">peak-threads</a>&#8221;. Feel free to check it out there.</p><p>The license is MPL 2.0 - meaning you can use the project in commercial or non-commercial without releasing your code. Only direct changes to the library itself need to be public (so if make a change/bug fix, that change/bug fix needs to be shared somewhere<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>). I chose this license since it&#8217;s a nice blend between allowing commercial closed-source products to use the code, while also allowing the library itself to remain open.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>The &#8220;type: module&#8221; simply says &#8220;spawn this thread with ESM module support&#8221; - which is needed if you import my library with &#8220;import&#8221; rather than using &#8220;importScripts&#8221; and specifying a url to the IIFE bundle. In other words, using ESM &#8220;import&#8221; means you get an ESM module. Using &#8220;importScripts&#8221; and IIFE means you get a good old-fashion JavaScript library.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Technically, GitHub is a mirror, but I&#8217;m using it for issue tracking so that&#8217;s why I list it first.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>By &#8220;somewhere&#8221; I really do mean &#8220;somewhere&#8221;. It does NOT have to be a direct contribution to my repository, or to someone&#8217;s fork, or a community repository, or whatever. It doesn&#8217;t even have to be on the internet. It could be on a floppy drive or a piece of paper you mail to someone. That&#8217;s allowed by the license. I don&#8217;t really care where you put the modified copy. The goal is to make sure that people are sharing their changes to a free, publicly available library and not hoarding those changes. As for the end product - make money off of it. Keep the rest of your code private. Lock it in an underground vault. Do whatever. That&#8217;s you&#8217;re code. My code that is used stays open.</p></div></div>]]></content:encoded></item><item><title><![CDATA[WaitGroups in JavaScript]]></title><description><![CDATA[Signalling to the main thread without message passing]]></description><link>https://matthewtolman.com/p/waitgroups-in-javascript</link><guid isPermaLink="false">https://matthewtolman.com/p/waitgroups-in-javascript</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Sun, 01 Mar 2026 01:25:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So far in my JavaScript threading series, we&#8217;ve covered <a href="https://matthewtolman.com/p/playing-with-threads-in-javascript">spawning threads &amp; passing messages</a>, <a href="https://matthewtolman.com/p/javascript-mutexes">mutexes</a>, and <a href="https://matthewtolman.com/p/javascript-condition-variables">condition variables</a>. We can now start to have signaling and synchronization between threads, and we could start trying to make higher-level abstractions on-top of what we have, such as channels (<a href="https://archives.matthewtolman.com/articles/2025-04-building-channels-in-cpp.html">which is something I did in C++ a while back</a>).</p><p>However, before we do, there is one additional primitive I want to introduce, and that is <a href="https://pkg.go.dev/sync#WaitGroup">Go-style WaitGroups</a>. Currently, we are still using message passing in our examples to tell the main thread that our thread&#8217;s work is &#8220;done&#8221; and now they can read the value. Which really undermines the whole reason we went through all the trouble to share memory in the first place. If we&#8217;re still going to pass messages to signal the main thread<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, then have we gained anything from sharing memory?</p><p>We <em>could</em> use a Condition Variable to signal things - but then we would also need to manage a mutex as well. Instead, what we want is a signaling primitive that is stand-alone - no mutex needed.</p><p>This is where Go&#8217;s WaitGroups come in. They are basically a waitable-counter. The main thread sets the initial value of the counter, and then waits until that counter hits zero. All of the other threads decrement the counter once they&#8217;re done with their work.</p><p>Here&#8217;s an example of how this would look in JavaScript:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;99d21cb5-c12d-4438-adf3-c81ee4303542&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// This would be in your main code
async function mainThread() {
  // Setup our memory and wait group
  const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 3)
  const ints = new Int32Array(sab)
  const wg = new WaitGroup(ints, 0)

  // Initialize our thread
  const worker = new Worker('my-worker.js')

  // Setup our work
  wg.add(1)
  worker.postMessage({__type: 'square', wg: 0, mem: ints, input: 3, dest: 1})

  // Setup another work item
  wg.add(1)
  worker.postMessage({__type: 'cube', wg: 0, mem: ints, input: 4, dest: 2})

  // Wait for the work to get done
  await wg.waitAsync()

  // Read our results
  console.log('Square of 3: ', ints.at(1))
  console.log('Cube of 4: ', ints.at(2))
}</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;b92b0d0a-a8a5-43cc-9402-999c9f45c423&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// my-worker.js

onmessage = (msg) =&gt; {
  if (msg.__type === 'square') {
     const v = msg.input
     msg.mem.set([v * v], msg.dest)
     new WaitGroup(msg.mem, msg.wg).done()
  }
  else if (msg.__type === 'cube') {
     const v = msg.input
     msg.mem.set([v * v * v], msg.dest)
     new WaitGroup(msg.mem, msg.wg).done()
  }
}</code></pre></div><p>With this code, we can now send tasks off to the worker thread, tell it where we want the results written, and then wait for the tasks to get done. Once the tasks are done, we simply read the memory. Everything is also self-contained, in the message<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>, so we can simply</p><p>In a way, this is <a href="https://matthewtolman.com/p/playing-with-threads-in-javascript">simpler than the outstanding work map that stored Promise resolvers</a> which we had before. All of the control flow for the main thread is linear. There is no magic data structure somewhere to correlate sent messages with received messages, and there&#8217;s no weird onmessage handler which then uses said data structure. Instead, we send some work, we wait for it, and we read the result. Very beautiful.</p><p>On the worker-side, it&#8217;s also fairly simple. We just call <code>done</code> when we&#8217;re done, rather than post a response back. We don&#8217;t have to parse and propagate a message id either. We call <code>done</code> and it takes care of the rest.</p><h2>Actually Making a WaitGroup</h2><p>So, now that we see how we would use a WaitGroup, lets&#8217;s make one!</p><p>Fortunately, they&#8217;re very simple and only require a few atomics. No mutexes or condition variables necessary! (Hence why I call it a primitive).</p><p>Here&#8217;s the code in TypeScript:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;7ec82de0-66f6-4d7d-9fce-5c9b5c36fc82&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">class WaitGroup {
    private memory: Int32Array
    private offset: number

    constructor(memory: Int32Array, offset: number) {
        this.memory = memory
        this.offset = offset
    }

    public add(count: number = 1) {
        // Very simple, just an atomic add
        Atomics.add(this.memory, this.offset, count)
    }

    public done() {
        // Perform a check if we should notify the waiter
        // We'll notify the waiter of we set the value to zero
        // Due to how atomics return the *old* value, that means
        // for a subtract we send when the old value was 1
        if (Atomics.sub(this.memory, this.offset, 1) === 1) {
            Atomics.notify(this.memory, this.offset)
        }
    }

    public wait(timeout: number = Infinity) {
        let lastTime = Date.now()

        // Loop until our counter hits zero
        while (true) {
            // Load our counter and see if we're zero!
            const cur = Atomics.load(this.memory, this.offset);
            if (cur == 0) {
                return true;
            }

            // Suspend when we're not zero
            if (Atomics.wait(this.memory, this.offset, cur, timeout) === 'timed-out') {
                return false
            }

            // remember to update the timeout value whenever we loop!
            if (Number.isFinite(timeout)) {
                let curTime = Date.now()
                let elapsed = curTime - lastTime
                timeout -= elapsed
                lastTime = curTime
                if (timeout &lt;= 0) {
                    return false
                }
            }
        }

    }

    public async waitAsync(timeout: number = Infinity) {
        // Same thing as above, but with promises now!
        let lastTime = Date.now()
        while (true) {
            const cur = Atomics.load(this.memory, this.offset);
            if (cur == 0) {
                return true;
            }

            // Yay promises!
            const {async, value} = (Atomics as any).waitAsync(this.memory, this.offset, cur, timeout)
            if (async) {
                if (await value === 'timed-out') {
                    return false
                }
            } else if (value === 'timed-out') {
                return false
            } else {
                // Always ensure we suspend for at least one micro-tick per cycle
                await new Promise(res =&gt; res(null))
            }


            if (Number.isFinite(timeout)) {
                let curTime = Date.now()
                let elapsed = curTime - lastTime
                timeout -= elapsed
                lastTime = curTime
                if (timeout &lt;= 0) {
                    return false
                }
            }
        }
    }
}</code></pre></div><p>The general idea is pretty straightforward:</p><ul><li><p>When we call &#8220;add&#8221;, we atomically increment the counter</p></li><li><p>When we call &#8220;done&#8221;, we atomically decrement the counter</p><ul><li><p>We&#8217;ll also signal once we hit zero</p></li></ul></li><li><p>When we call &#8220;wait&#8221;, we just loop until the counter hits zero while guaranteeing a suspend in every iteration</p></li></ul><p>Pretty straightforward stuff.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Unfortunately, due to how the standard operates the main thread will always need to send at least one message to the worker. This is due to the fact that the standard requires us to use message passing to pass in shared memory. So, when I say &#8220;remove message passing&#8221; I&#8217;m talking about removing the messages sent from the worker to the main/parent thread.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>We do have to reconstruct the wait group inside the worker simply because classes and class instances cannot be passed with message passing. We could use some very clever overrides of global methods to make the system automated - however we would still be reconstructing it on the other end. For explicitness, I&#8217;ll leave the manual reconstructions in-plain sight for this series.</p></div></div>]]></content:encoded></item><item><title><![CDATA[JavaScript Condition Variables]]></title><description><![CDATA[Waiting for conditions to change]]></description><link>https://matthewtolman.com/p/javascript-condition-variables</link><guid isPermaLink="false">https://matthewtolman.com/p/javascript-condition-variables</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Mon, 16 Feb 2026 01:29:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a continuation of my threading in JavaScript series.</p><p>So far we&#8217;ve <a href="https://matthewtolman.com/p/better-mutexes-in-javascript?r=2alomj&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true&amp;triedRedirect=true">created locks</a>, which lets us make sure that only one thread accesses <a href="https://matthewtolman.com/p/sharing-memory-across-threads-in">shared memory</a> at a time. We&#8217;ve also updated the locks to have promises, so we can use it from the main thread. However, we don&#8217;t have a way to signal to the main thread that our work is done - at least not using shared memory<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. What we need is a way to signal to other threads. And we need something more robust and complete than simply a futex.</p><p>This is where condition variables come in. They let threads wait until a <em>condition</em> changes, not just when a memory address changes. These conditions can be complex or simple, and the developer (us) gets to define them. For instance, we can wait until the account balance for a user is below a threshold, or we can wait until a queue has items or is empty. We could wait for other threads to finish their work. There are lots of possibilities!</p><p>Of course, when we&#8217;re dealing with so many possibilities, it usually means we&#8217;re dealing with a primitive, and that we&#8217;ll need to build up those possibilities ourselves. Which is true, condition variables are a synchronization primitive. But, once we have a primitive, we can learn the patterns around using that primitive (which I&#8217;ll cover more next post).</p><p>So, let&#8217;s get started.</p><h2>Condition Variable Usage</h2><p>Before we get too far into the implementation details, let&#8217;s look at how a condition variable will work. Here&#8217;s an example C program to highlight the usage patterns:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;1ebcd8c1-cb1c-4286-b7e3-2549b70174ef&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">typedef struct {
    Mutex   lock; // mutex for locking shared data
    CondVar sendCv; // Condition variable to wait for data to be sendable
    CondVar recvCv; // Condition variable to wait for data to be receiveable
    int     message; // Data to send/receive
} SharedQueue;

int receive(SharedQueue* q) {
    mutex_lock(&amp;q-&gt;lock); // lock our mutex

    // wait for a message
    while (q-&gt;message == 0) {
        // notice that we don't unlock here
        cond_var_wait(&amp;q-&gt;recvCv, &amp;q-&gt;lock);
    }

    // read our message
    printf("Received: %d\n", q-&gt;message);
    int res = q-&gt;message;
    q-&gt;message = 0;

    // Wake up a single waiter waiting to send
    cond_var_notify(&amp;q-&gt;sendCv, 1);

    // now we unlock
    mutex_unlock(&amp;q-&gt;lock);
   
    return res;
}

void send(SharedQueue* q, int msg) {
    mutex_lock(&amp;q-&gt;lock); // lock our mutex

    // wait for the queue to be empty
    while (q-&gt;message != 0) {
        // we don't unlock here either
        cond_var_wait(&amp;q-&gt;sendCv, &amp;q-&gt;lock);
    }

    printf("Sending: %d\n", msg);

    // send the message
    q-&gt;message = msg;

    // notify someone that the message was sent
    cond_var_notify(&amp;q-&gt;recvCv, 1);

    // unlock
    mutex_unlock(&amp;q-&gt;lock);
}</code></pre></div><p>Things look a little odd. We&#8217;re getting the lock so no one else can change things, but then we hold onto the lock while waiting for someone to change things. It appears that we have a deadlock. Except, we don&#8217;t.</p><p>What the above example doesn&#8217;t show is how the condition variable works. It turns out, when we call wait on a condition variable the condition variable will unlock the lock. That&#8217;s why we have to pass in the lock with the wait call - so the condition variable knows what to unlock.</p><p>But, wait, if it unlocks, then why aren&#8217;t we relocking when the method returns? Simply put, the condition variable will lock immediately before returning.</p><p>This &#8220;lock/unlock&#8221; inside a condition variable is what allows the condition to change, all while our code runs inside a locked context. Neat!</p><p>Though, you may be wondering what happens with the notify. We&#8217;re notifying before we unlock. Shouldn&#8217;t we switch the order?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;2ae21c5a-4fe7-4820-9f10-f6b4c86fd791&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">    // unlock
    mutex_unlock(&amp;q-&gt;lock);

    // notify someone that the message was sent
    cond_var_notify(&amp;q-&gt;recvCv, 1);</code></pre></div><p>Well, we definitely could! Both patterns are generally valid. The only difference is where the other thread is waiting. The other thread is either waiting on the condition variable signal, or it&#8217;s waiting on the lock&#8217;s unlock signal. In both cases, it can&#8217;t proceed until we both unlock and notify.</p><p>One other thing to note, we have the whole &#8220;while&#8221; loop around the condition variable, but wasn&#8217;t the point to notify when something was done?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;3ead4bca-b050-4f47-85e6-b182b832addb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">    // wait for a message
    while (q-&gt;message == 0) {
        // notice that we don't unlock here
        cond_var_wait(&amp;q-&gt;recvCv, &amp;q-&gt;lock);
    }</code></pre></div><p>Well, yes. But, we&#8217;re doing two separate atomic operations. We&#8217;re first notifying a thread, and then locking a mutex (or vice versa). Between those two atomic operations, another thread could come right on in and change the condition again (in this case, steal our message!). To handle that scenario, we need to loop and retry.</p><p>With condition variables, and pretty much any synchronization primitive, it&#8217;s very important for us to understand how it should be used. The usage will help drive the implementation, especially when we have to consider all sorts of edge cases - like threads stealing our state.</p><p>Now that we&#8217;ve covered the basics of how condition variables are used, let&#8217;s start making one!</p><h2>Building a Condition Variable</h2><p>There are two main operations we&#8217;ll be doing with a condition variable:</p><ul><li><p>Wait</p></li><li><p>Notify</p></li></ul><p>Our wait will have an async (Promise-based) and synchronous (blocking) version - both of which will have timeouts. Our notify will allow us to specify how many waiters we want to notify. We&#8217;ll have separate async/sync versions of the wait be separate methods. This gives us three methods in total. Here&#8217;s our scaffolding:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;fa98ee6a-94f1-456f-b72f-c33ac55763ff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">function CondVariable() {
    // initialize here
    return {
        // mutex from previous posts
        wait: (mux) =&gt; {
            // TODO
        },
        waitAsync: async (mux) =&gt; {
            // TODO
        },
        notify: (count) -&gt; {
           // TODO
        }
    }
}</code></pre></div><p>We&#8217;ll start with the blocking wait and notify methods first, and then we&#8217;ll make the other methods from that. Also, let&#8217;s get things working before we do a timeout. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://matthewtolman.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://matthewtolman.com/subscribe?"><span>Subscribe now</span></a></p><h3>Blocking Wait</h3><p>Let&#8217;s start off with the lock/unlock code first in our wait method, and stub the other data.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;019aa352-57bb-4a44-b06e-9b82d236e676&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">wait: (mux) =&gt; {
    // TODO: do something here to indicate we're waiting
   
    mux.unlock()

    // TODO: do something here to wait until something changes

    mux.lock()
}</code></pre></div><p>Well, so far so good. But now we have a problem. We need some sort of internal state that we can share across threads<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. </p><p>So, we&#8217;ll need to update our constructor to take in some memory and offset positions. We&#8217;re going to use two pieces of state. The very first piece is going to be the counter we&#8217;re waiting on. The second piece of state is a safeguard to make sure our counter becomes a different value after we started waiting<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. The safeguard becomes needed once we start getting more threads involved.</p><p>So, let&#8217;s update our constructor:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;c67d5760-7276-4c27-8756-c5e21bfe360b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// prevOffset is used to mitigate wrapping behaviors
// valOffset is used to wakeup our thread
function CondVariable(memory, prevOffset, valOffset) {
    // initialize here
    return {</code></pre></div><p>Now that we have our memory, let&#8217;s make our wait function. What we&#8217;re going to do is take our value that we&#8217;re waiting on, store it in our previous counter, and then wait for our value to change. By storing the previous value, we&#8217;ll allow our notifier to know what we waited on, and guarantee we get something unique regardless of how many threads are competing. Here&#8217;s the code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;2355af51-3a6a-4684-bb7e-1624e2cfc258&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">wait: (mux) =&gt; {
    const val = Atomics.load(memory, valOffset)
    Atomics.store(memory, prevOffset, val)
   
    mux.unlock()

    Atomics.wait(memory, valOffset, val)

    mux.lock()
}</code></pre></div><p>Now let&#8217;s write our notification code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;a7d0d501-907f-421a-84ee-152b6c862bc6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">notify: (couunt) =&gt; {
    const val = Atomics.load(memory, prevOffset)
    Atomics.store(memory, valOffset, (val + 1) | 0)
    Atomics.notify(memory, valOffset)
}</code></pre></div><p>Notice something odd about this? We store the current value into previous, and then when we notify we do one more than previous, not one more than the current address.</p><p>The reason for this is to &#8220;rollback&#8221; when there are multiple waiters. Each waiter only cares if the value is different from &#8220;previous&#8221; when it&#8217;s woken up, not if it&#8217;s different from &#8220;previous + 1&#8221;. So, we can reuse the same value (&#8220;previous + 1&#8221;) to wake up all of our threads.</p><p>There&#8217;s also another odd thing. We&#8217;re doing &#8220;(val + 1) | 0&#8221;. This <a href="https://archives.matthewtolman.com/articles/unsigned-integers-in-javascript.html">emulates signed 32-bit integers</a> which gives us very specific overflow patterns - in this case, two&#8217;s complement wrapping. If we didn&#8217;t do this, at a certain point we&#8217;d hit the maximum &#8220;safe integer&#8221; range in JavaScript and we&#8217;d get stuck at the same number forever. So, it&#8217;s best that we define the wrapping mechanism.</p><p>The async code is very similar. Here it is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;e7a0405e-6d49-41eb-8a13-163172721439&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">waitAsync: async (mux) =&gt; {
    const val = Atomics.load(memory, valOffset)
    Atomics.store(memory, prevOffset, val)
   
    mux.unlock()

    const {async, value} = Atomics.waitAsync(memory, valOffset, val)
    if (async) { await value }
    else { await new Promise(r =&gt; r()) }

    await mux.lockAsync()
}</code></pre></div><h3>Async and Timeouts</h3><p>Adding timeouts is very straightforward. We don&#8217;t have any loops, but we do have two separate calls that need timeouts. We&#8217;ll still do the timeout adjustment.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;9e09e28f-ba23-4ffd-97a7-347bfb50b739&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">wait: (mux, timeout = Infinity) =&gt; {
    const start = Date.now()
    const val = Atomics.load(memory, valOffset)
    Atomics.store(memory, prevOffset, val)
   
    mux.unlock()

    if (Atomics.wait(memory, valOffset, val, timeout) === 'timed-out') {
        return false
    }

    if (!Number.isFinite(timeout)) {
        const end = Date.now()
        const elapsed = end - start
        timeout -= elapsed
        if (timeout &lt;= 0) return false;
    }

    mux.lock(timeout)
    return true
},
waitAsync: async (mux, timeout = Infinity) =&gt; {
    const start = Date.now()
    const val = Atomics.load(memory, valOffset)
    Atomics.store(memory, prevOffset, val)
   
    mux.unlock()

    const {async, value} = Atomics.waitAsync(memory, valOffset, val, timeout)
    if (async) {
        if (await value === 'timed-out') return false
    }
    else if (value === 'timed-out') return false
    else await new Promise(r =&gt; r())

    if (!Number.isFinite(timeout)) {
        const end = Date.now()
        const elapsed = end - start
        timeout -= elapsed
        if (timeout &lt;= 0) return false;
    }

    return await mux.lockAsync(timeout)
}</code></pre></div><h2>Wrap Up</h2><p>Well, that&#8217;s it! We&#8217;ve created a new synchronization primitive. Obviously, we need to spend more time figuring out how to use it (that&#8217;s next time!). However, between atomics, futexes, mutexes and condition variables we can basically create any other primitive we want. We can even start creating some more &#8220;modern&#8221; synchronization patterns, like Go&#8217;s <a href="https://pkg.go.dev/sync#WaitGroup">WaitGroup</a> and <a href="https://go.dev/tour/concurrency/2">Channels</a>, or C++20&#8217;s <a href="https://en.cppreference.com/w/cpp/thread/barrier.html">barrier</a>. Read/Write locks are also something we can make.</p><p>All of which I&#8217;ll be getting to in future posts, so stay tuned!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://matthewtolman.com/p/javascript-condition-variables?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://matthewtolman.com/p/javascript-condition-variables?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>We can do the built-in <code>postMessage</code> and <code>onmessage</code> <a href="https://matthewtolman.com/p/playing-with-threads-in-javascript">style message passing</a>, but we&#8217;re trying to move beyond that and do everything with <code>SharedArrayBuffers</code>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>In fact, we&#8217;ll need <em>two </em>pieces of internal state.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Remlab has a great article about why the two variables are needed: <a href="https://www.remlab.net/op/futex-condvar.shtml">https://www.remlab.net/op/futex-condvar.shtml</a></p></div></div>]]></content:encoded></item><item><title><![CDATA[Better Mutexes in JavaScript]]></title><description><![CDATA[Adding timeouts and async to our synchronization]]></description><link>https://matthewtolman.com/p/better-mutexes-in-javascript</link><guid isPermaLink="false">https://matthewtolman.com/p/better-mutexes-in-javascript</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Sun, 15 Feb 2026 02:29:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously we talked about some of the issues that <a href="https://matthewtolman.com/p/deadlocks-and-starvation-in-javascript">can happen when doing multi-threading</a>. Some of the mitigations involved used timeouts or &#8220;try&#8221; locks without actually blocking. One other issue we didn&#8217;t really discuss is that we can&#8217;t call blocking atomic methods (i.e. <code>Atomics.wait</code>) inside our main thread (basically our main JavaScript code).</p><p>If we take a step back and <a href="https://matthewtolman.com/p/javascript-mutexes">look at the mutexes we&#8217;ve made previously</a>, we&#8217;ll notice that we&#8217;re lacking on all of those fronts. We block, so our main thread can&#8217;t be used. We don&#8217;t offer timeouts. And we don&#8217;t offer a &#8220;try&#8221; to lock mechanism.</p><p>Let&#8217;s fix that.</p><p>For a refresher, here&#8217;s our mutex code (with some minor cleanup to be more readable):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;24c1739c-ffa5-4164-96b3-e942c86ddaaa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">function mutex(memory, offset) {
    const unlocked = 0
    const locked = 1
    const contended = 2
    return {
        lock: () =&gt; {
            // Try to get the lock (will only lock if we're unlocked)
            let cur = Atomics.compareExchange(memory, offset, unlocked, locked)
            if (cur === unlocked) {
                return // got the lock
            }

            while(true) {
                // signal contention
                if (cur !== contended) {
                    Atomics.compareExchange(memory, offset, locked, contended)
                }

                // Wait until we're unlocked
                Atomics.wait(memory, offset, contended)

                // try to lock again
                cur = Atomics.compareExchange(memory, offset, unlocked, contended)

                if (cur === unlocked) {
                    return // got the lock
                }
            }
        },
        unlock: () =&gt; {
            // try to unlock
            if (Atomics.sub(memory, offset, 1) !== locked) {
                // Lock was contended, so we need to unlock and signal
                Atomics.store(memory, offset, unlocked)
                Atomics.notify(memory, offset, 1)
            }
        }
    }
}</code></pre></div><p>So, now we need to create two new methods: <code>tryLock</code> and <code>lockAsync</code>. We also need to insert a timeout as an optional parameter to both <code>lock</code> and <code>lockAsync</code>.</p><h2>tryLock</h2><p>Try lock is fortunately very easy. All we do is try to lock without doing the loop. If we fail to lock, we return false, otherwise we return true. We can do this by simply copying our lock code up until the while loop, and then adjust the return values. Here&#8217;s tryLock.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;fa4e472d-1ee5-4958-aa1b-bbb8314d53b8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">    tryLock: () =&gt; {
            // Try to get the lock (will only lock if we're unlocked)
            let cur = Atomics.compareExchange(memory, offset, unlocked, locked)
            if (cur === unlocked) {
                return true // got the lock
            }
            return false // didn't get the lock
        },</code></pre></div><h2>lockAsync</h2><p>Making an async version of our lock method is also fairly straightforward. We mostly need to swap our <code>Atomics.wait</code> call with <code>Atomics.waitAsync</code>, and then await the promise we get back.</p><p>Except, we don&#8217;t always get a promise back. So, we can only await sometimes.</p><p>If we <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync">look at the documentation</a>, we&#8217;ll get an object back with an <code>async</code> flag, and a <code>value</code>. If the <code>async</code> flag is true, then <code>value</code> has a Promise. Otherwise, it has a string.</p><p>With that information, we can now create an async lock method.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;a38b4732-d45a-4dc6-bfa3-28c5cda2c046&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">        lockAsync: async () =&gt; {
            // Try to get the lock (will only lock if we're unlocked)
            let cur = Atomics.compareExchange(memory, offset, unlocked, locked)
            if (cur === unlocked) {
                return // got the lock
            }

            while(true) {
                // signal contention
                if (cur !== contended) {
                    Atomics.compareExchange(memory, offset, locked, contended)
                }

                // Wait until we're unlocked
                const {async, value} = Atomics.waitAsync(memory, offset, contended)
                if (async) {
                    await value
                } else {
                    // Suspend for one micro-tick
                    await new Promise(res =&gt; res())
                }

                // try to lock again
                cur = Atomics.compareExchange(memory, offset, unlocked, contended)

                if (cur === unlocked) {
                    return // got the lock
                }
            }
        },</code></pre></div><h2>Adding Timeouts</h2><p>Adding timeouts is a little trickier, but not by much. Both <code>wait</code> and <code>waitAsync</code> will provide a timeout value on a timeout, so we&#8217;ll just check the result (in the <code>waitAsync</code> case it&#8217;s the <code>value</code> field when <code>async</code> is false). We&#8217;ll also need to check the result of awaiting the promise, in case that times out. Once we know if we&#8217;ve timed out or not, we will need to copy the &#8220;true/false&#8221; behavior from <code>tryLock</code>.</p><p>The only tricky thing is what happens when we loop. If we failed to get the lock after a wait, then we need to update our timeout based on how much time has passed. We&#8217;ll do that by tracking the start time, and then at the end of the loop we&#8217;ll query the current time. We&#8217;ll then subtract the start time from the current time to get the elapsed time. As for the timeout, we&#8217;ll default the timeout value to <code>Infinity</code> - which means no timeout.</p><p>Let&#8217;s take a look at how to adjust the <code>lock</code> function (<code>lockAsync</code> is very similar).</p><p>Similar adjustments can be made for the <code>lockAsync</code> function.</p><p>With this, we&#8217;ve added timeouts to our mutex.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;420ecc66-d6d3-4048-9b21-05f00f227b05&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">        lock: (timeout = Infinity) =&gt; {
            // Try to get the lock (will only lock if we're unlocked)
            let cur = Atomics.compareExchange(memory, offset, unlocked, locked)
            if (cur === unlocked) {
                return true // got the lock
            }

            // Time to track the time to make sure we can timeout when needed
            let lastTime = Date.now()

            while(true) {
                // signal contention
                if (cur !== contended) {
                    Atomics.compareExchange(memory, offset, locked, contended)
                }

                // Wait until we're unlocked, or until we timeout
                const res = Atomics.wait(memory, offset, contended, timeout)

                // Check for a timeout
                if (res === 'timed-out') {
                    return false; // timed out
                }

                // try to lock again
                cur = Atomics.compareExchange(memory, offset, unlocked, contended)

                if (cur === unlocked) {
                    return true // got the lock
                }

                // Check elapsed time and then update the timeout (if we have one)
                if (Number.isFinite(timeout)) {
                    const curTime = Date.now()
                    const elapsed = curTime - lastTime
                    timeout -= elapsed

                    // Make sure we didn't timeout
                    if (timeout &lt;= 0) {
                        return false // timed out
                    }
                }
            }
        },</code></pre></div><h2>Wrap Up</h2><p>We&#8217;ve added try locks, async, and timeouts to our mutexes, making them far more robust and versatile to use for our JavaScript code.</p><p>For reference, here&#8217;s the full mutex with all of our improvements.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;0e5d225a-7b09-4d1e-8f6d-21ca976b92b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">function mutex(memory, offset) {
    const unlocked = 0
    const locked = 1
    const contended = 2
    return {
        /**
         * Locks the mutex (blocking)
         *
         * If given a timeout, then it will try to lock before the timeout occurs, otherwise it will fail to lock
         *
         * @param timeout Timeout (in milliseconds) for obtaining the lock
         * @returns {boolean} True if got the lock, false if timed out
         */
        lock: (timeout = Infinity) =&gt; {
            let cur = Atomics.compareExchange(memory, offset, unlocked, locked)
            if (cur === unlocked) { return true /* got the lock */ }
            let lastTime = Date.now()

            while(true) {
                if (cur !== 2) {
                    Atomics.compareExchange(memory, offset, cur, contended)
                }
                const r = Atomics.wait(memory, offset, contended, timeout)
                if (r === "timed-out") {
                    return false
                }

                cur = Atomics.compareExchange(memory, offset, unlocked, contended)
                if (cur === unlocked) {
                    return true /* got the lock */
                }

                if (Number.isFinite(timeout)) {
                    let curTime = Date.now()
                    let elapsed = curTime - lastTime
                    timeout -= elapsed
                    lastTime = curTime
                    if (timeout &lt;= 0) {
                        return false
                    }
                }
            }
        },
        /**
         * Asynchronously locks a mutex.
         * Returns a promise which resolves to true if the lock was obtained, or false otherwise
         * @param timeout Timeout (in milliseconds) for obtaining the lock
         * @returns {Promise&lt;boolean&gt;} Promise that resolves to true if got the lock, false if timed out
         */
        lockAsync: async (timeout = Infinity) =&gt; {
            let cur = Atomics.compareExchange(memory, offset, unlocked, locked)
            if (cur === unlocked) { return true /* got the lock */ }
            let lastTime = Date.now()

            while(true) {
                if (cur !== 2) {
                    Atomics.compareExchange(memory, offset, cur, contended)
                }
                const {async, value} = Atomics.waitAsync(memory, offset, contended, timeout)
                if (async) {
                    const r = await value
                    if (r === 'timed-out') {
                        return false
                    }
                }
                else if (value === 'timed-out') {
                    return false
                } else {
                    await new Promise(res =&gt; res())
                }

                cur = Atomics.compareExchange(memory, offset, unlocked, contended)
                if (cur === unlocked) {
                    return true /* got the lock */
                }

                if (Number.isFinite(timeout)) {
                    let curTime = Date.now()
                    let elapsed = curTime - lastTime
                    timeout -= elapsed
                    lastTime = curTime
                    if (timeout &lt;= 0) {
                        return false
                    }
                }
            }
        },
        /**
         * Tries to get a lock without waiting. Only locks if the mutex is unlocked and not contended
         * @returns {boolean} True if it got the lock, false otherwise
         */
        tryLock: () =&gt; {
            // Try to get the lock (will only lock if we're unlocked)
            let cur = Atomics.compareExchange(memory, offset, unlocked, locked)
            if (cur === unlocked) {
                return true // got the lock
            }
            return false // didn't get the lock
        },
        /**
         * Unlocks the mutex
         */
        unlock: () =&gt; {
            if (Atomics.sub(memory, offset, 1) !== locked) {
                Atomics.store(memory, offset, unlocked)
                Atomics.notify(memory, offset, 1)
            }
        }
    }
}</code></pre></div>]]></content:encoded></item><item><title><![CDATA[Deadlocks and Starvation in JavaScript]]></title><description><![CDATA[Threads in the browser are still threads]]></description><link>https://matthewtolman.com/p/deadlocks-and-starvation-in-javascript</link><guid isPermaLink="false">https://matthewtolman.com/p/deadlocks-and-starvation-in-javascript</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Tue, 10 Feb 2026 01:30:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously I showed how to <a href="https://matthewtolman.com/p/javascript-mutexes">share memory between threads and create mutexes inside of JavaScript</a>. Which is really cool since it avoids the overhead of creating, queuing, and polling messages when passing simple data. And it can make it a lot easier for multiple threads to share data (the alternatives are broadcasting data or doing a map-reduce model).</p><p>However, not all is calm in multi-threading land. As soon as we bring multiple threads in, we get a whole host of problems (data races, logic races, starvation, etc.). And now that we&#8217;ve brought in locks, we get even more problems (deadlocks). Before we get too much further into all the cool ways to create primitives or to do cool things with threads, I wanted to take a step back. The rest of our journey will have many perils, so let&#8217;s rest and tell tales before continuing.</p><h2>Starvation</h2><p>Starvation is when greedy threads repeatedly take the resources from other threads, thereby blocking (or starving) them from making any progress. Signs of starvation include some threads perpetually hanging and some tasks being unusually delayed while other tasks are going through just fine. Another sign is tasks that are way too old to be running end up getting completed (and sometimes corrupting data) of more recent tasks.</p><p>Depending on the environment and severity, starvation can start to affect other resource uses than just CPU time or task completion time. Memory usage and disk usage can grow as work gets queued but threads don&#8217;t get resources to process them. </p><p>Thread starvation is related to thread contention, but in the way that starvation is a more serious (and dangerous) condition than contention. Contention is when all threads still make some progress, but many (or all) are getting slowed down by synchronization (aka. your program runs slower). Starvation is when the contention gets so out-of hand that some threads are unable to make <em>any</em> progress.</p><p>The risks and dangers of starvation is very real, <a href="https://aws.amazon.com/message/101925/">even for massive tech companies</a>. And, paradoxically, some of the same patterns that help with contention increase the risks of starvation.</p><h3>Common Patterns which cause Starvation</h3><p>One such pattern is exponential back-off. The idea is simple, if a resource is highly shared, then having every thread constantly trying it can make every thread slower. Additionally, sometimes the resource has limits (rate limits, throughput limits, etc.) so hammering the resource at the same rate can overwhelm the resource. The most common solution to this is to simply &#8220;back-off&#8221; with longer and longer time gaps, that way if the resource is contended we give it additional time to be in a non-contended state. Often this is achieved by doubling (or some other constant multiplication) the wait period. The doubling is why this is called &#8220;exponential&#8221; back-off as the wait period grows exponentially, which decreases the load on the contended resource massively. Sounds good, right?</p><p>Well, almost. What happens when a resource is highly contended? What happens when a resource is locked 80% or 90% or 99% of the time? In these scenarios, when a thread tries to get a lock, it will fail to get a lock 80% or 90% or 99% of the time. If it fails to get a lock, it will wait longer and longer between each retry.</p><p>Exponential back-offs become very long very quick. At an initial 1ms delay and a doubling each time, it only takes 10 retries before it&#8217;s been a full second. At 1 second, modern CPUs have wasted 3-4 billion cycles. After 16 retries, it&#8217;s been over a minute. That&#8217;s 180-240 billion cycles.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!m_p3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!m_p3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 424w, https://substackcdn.com/image/fetch/$s_!m_p3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 848w, https://substackcdn.com/image/fetch/$s_!m_p3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 1272w, https://substackcdn.com/image/fetch/$s_!m_p3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!m_p3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png" width="879" height="664" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/401759f7-e978-413a-9a44-77b896c6ebad_879x664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:664,&quot;width&quot;:879,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:34775,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://matthewtolman.com/i/187334601?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!m_p3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 424w, https://substackcdn.com/image/fetch/$s_!m_p3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 848w, https://substackcdn.com/image/fetch/$s_!m_p3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 1272w, https://substackcdn.com/image/fetch/$s_!m_p3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F401759f7-e978-413a-9a44-77b896c6ebad_879x664.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Another common cause is simply over-allocating threads or over sharing data between threads. More threads doesn&#8217;t always make things faster, and instead can make things slower due to contention. The more threads, the more contention, and the more likely some poor thread gets repeatedly locked.</p><p>More shared data isn&#8217;t good either, and it may be more acceptable to duplicate and copy data than to share it. Generally, it&#8217;s best to minimize the amount of data shared between threads as much as possible.</p><p>Other common patterns involve overuse of read-write locks, over locking or locking for extended periods of time, and doing network requests or expensive computation tasks while locked.</p><p>Read-write locks are an optimization that sounds good (and is often very good for usual performance). However, they have a dark side too. Many read-write locks are not &#8220;fair&#8221; in that they don&#8217;t prioritize readers equally to writers. Instead, they will prefer one caste over the other - usually readers since they can be more concurrent. The segregation of read-write locks can cause massive issues if the preferred caste is extremely pervasive in usage. For instance, a read-preferring lock will starve write locks if there is always at least one active reader - a situation that can become very common in high-traffic scenarios.</p><p>Over locking is another issue where more locks are acquired than needed, or when locks are overused when simple atomic operations would have sufficed. Locks are blocking, and by being blocking they cause open up starvation points.</p><p>Holding locks for long periods of time also causes starvation. If a lock is held while performing expensive calculations or network requests, then that means no other thread can proceed until those calculations are done. If no other threads can proceed, then they end up creating a backlog of stuck threads. Stuck threads create contention, and that can lead to starvation.</p><h3>Mitigations</h3><p>The first mitigation is less about preventing starvation, and more about detecting it (and preventing it from causing outdated data to be propagated). It&#8217;s also rather simple: add enforced timeouts. If something takes too long, kill it and raise an alert. Get too many alerts, and there&#8217;s a starvation/high contention problem. The downside to this approach though, is that something you wanted to get done didn&#8217;t actually get done. That little fact can cause it&#8217;s own set of issues. But, in many scenarios, it&#8217;s far better for stale work to not get done then for it to get done and cause other issues.</p><p>A similar approach specific to retries (especially with exponential back-off) is to limit the number of retries allowed. If we only allow 5 retries before failing, then we guarantee we will succeed or fail in a fixed time period.</p><p>Another way to prevent starvation is to reduce contention. A simply solution is to reduce the number of threads (usually by using thread pools instead of spinning up a new thread per task). Other ways involve reorganizing code and memory access patterns to not require nearly as much sharing (prioritize independence over synchronization). Reducing the number of locks obtained (while maintaining correctness) also helps. Optimizing (or removing) work done in a locked section will help prevent threads from backing up. All of these strategies can involve new data structures (e.g. concurrent queues), large refactors of the code, or new algorithms and coding patterns (e.g. map-reduce).</p><p>Using wait-free algorithms and data structures can also help as they ensure that every thread makes progress. However, there are many drawbacks. Many wait-free algorithms have higher base latency, meaning a lock system with low contention can outperform a wait-free algorithm. Additionally, wait-free algorithms are hard to implement, hard to verify, and even harder to invent. Not every situation may have an appropriate wait-free solution.</p><h2>Data Races</h2><p>Data races are a very simple type of race condition. A program reads a value, modifies a value, and then that work becomes stale, and finally the stale value is written back.</p><p>Work becomes stale when another thread, process, or computer modifies the original value before the work was written back. We saw a <a href="https://matthewtolman.com/p/javascript-mutexes">very simple example</a> earlier when adding to the same address from different threads. However, this type of problem can arise in much more complex code or environments. It can happen with outdated cache layers, contention at the database layer, lack of idempotence in event processing, and lack of locks inside a program.</p><p>Generally, data races simply occur when there aren&#8217;t enough locks around shared data (in memory or on disk). Often, the solution is to either add more locks or reduce the need for shared memory. However, that&#8217;s not the only solution.</p><p>One other solution is to use more &#8220;compare exchange&#8221; semantics. Basically, perform an atomic read, do the calculation, and then do a compare exchange to write the answer back only if the value didn&#8217;t change. If it did change, read it, recalculate, and retry. This approach is called &#8220;optimistic concurrency&#8221; in that it&#8217;s optimistic a lock won&#8217;t be necessary since the value rarely changes.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> Of course, only use optimistic concurrency when that holds true, as unfounded optimism can lead to headaches when debugging production issues.</p><p>One side note, out of all the issues that we&#8217;ll talk about today, data races is the only concurrency issue Rust addresses when they are limited to inside a program&#8217;s memory! Data races across servers, cache layers, and databases aren&#8217;t covered by Rust&#8217;s compile time detection as it has no insight into those systems. High contention, starvation, logic races, and deadlocks aren&#8217;t covered either.</p><h2>Logic Races</h2><p>Logic races are similar to data races in that some work is stale (or incorrect) based on the work another thread did. However, the difference is that the race condition didn&#8217;t happen because some memory was changed instead of being locked. Instead, it has to do with a mismatched ordering of systems or tasks running.</p><p>To help clarify this distinction, we&#8217;ll use an example. Suppose that Jim has $30 in his account. He goes to the bank and deposits a $200 check. Simultaneously he purchases a $60 game on his phone while the banker is processing the check. In a data race, we would get a sequence like the following:</p><ul><li><p>Banker machine reads current balance of $30</p></li><li><p>Jim&#8217;s phone reads current balance of $30</p></li><li><p>Banker machine adds $200 to the balance resulting in $230</p></li><li><p>Jim&#8217;s phone deducts $60 from the balance resulting in ($30) and an overdraft fee</p></li><li><p>Banker machine writes $230 to the account</p></li><li><p>Jim&#8217;s phone writes ($30) to the account and an overdraft fee</p></li></ul><p>In this scenario, Jim&#8217;s $200 disappeared due to a data race - the actual data of his balance was not properly locked.</p><p>Now, let&#8217;s look at a logic race. We have two possible scenarios.</p><p><strong>Scenario 1</strong></p><ul><li><p>Banker machine lock&#8217;s Jim&#8217;s balance</p></li><li><p>Jim&#8217;s phone waits for lock</p></li><li><p>Banker machine updates Jim&#8217;s balance to $230 and unlocks</p></li><li><p>Jim&#8217;s phone locks Jim&#8217;s balance</p></li><li><p>Jim&#8217;s phone deducts $60, sets new balance to $170, and unlocks</p></li></ul><p><strong>Scenario 2</strong></p><ul><li><p>Jim&#8217;s phone locks&#8217;s Jim&#8217;s balance</p></li><li><p>Banker machine waits for lock</p></li><li><p>Jim&#8217;s phone sets Jim&#8217;s balance to ($30), adds an overdraft fee, and unlocks</p></li><li><p>Banker machine locks Jim&#8217;s balance</p></li><li><p>Banker machine sets Jim&#8217;s balance to $170 and unlocks</p></li></ul><p>In this scenario, we still end up with the correct final balance of $170. But, in Scenario 1 Jim doesn&#8217;t have an overdraft fee, yet in Scenario 2 he does! This difference based on ordering of simultaneous events is called a <em>logic race</em> and is a separate type of race condition from data races. What&#8217;s even more insidious is they&#8217;re incredibly hard to detect ahead of time<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>, and very hard to replicate.</p><p>To make matters worse, the code often appears correct during code reviews, and the code will run correctly during testing since it is correct when ran sequentially (i.e. banker first, then Jim&#8217;s phone). These types of bugs can also lay hidden for years in a production system where simultaneous events are extremely rare. And tracking these types of bugs down can be downright maddening as they appear and disappear like phantoms in the night. Validating a fix is also almost impossible. How do you check that a bug which only occurs once in tens of millions of button clicks is actually fixed?</p><p>The other worst part of these bugs is adding any other synchronization step (debugger, printing or logging messages, allocating memory) can be enough to add a reliable-enough ordering to prevent the bug from appearing inside a development or test environment. Truly maddening stuff.</p><h2>Deadlocks</h2><p>Deadlocks are one of the most dreaded concurrency bugs as they don&#8217;t just affect one thread or one customer. They can bring entire systems and products to their knees.</p><p>A deadlock is a simple. Multiple threads get stuck waiting for a lock they will never get. This isn&#8217;t starvation where they won&#8217;t get it for a long, long time or there&#8217;s a small probability that they won&#8217;t get it. No. This is <em>never</em>. As in, not even with <em>infinite</em> time.</p><p>There are a few ways a deadlock can happen. The first, is that a thread never calls <code>unlock</code> on a lock. Often, this happens because either the thread threw an exception and didn&#8217;t have the unlock in a finally block, or because the thread unexpectedly terminated prematurely - bypassing the finally block altogether<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. This is the easy case, both to create, find, and fix.</p><p>The other common way a deadlock happens is when multiple threads need multiple locks, but the way they try to get the locks causes errors.</p><p>For instance, assume we have two threads (1 and 2) and two locks (A and B).</p><ul><li><p>Thread 1 acquires lock B</p></li><li><p>Thread 2 acquires lock A</p></li><li><p>Thread 1 does some work, but realizes it now needs lock A</p></li><li><p>Thread 1 blocks on lock A, waiting for Thread 2 to finish</p></li><li><p>Thread 2 does some work, but realizes it cannot finish without lock B</p></li><li><p>Thread 2 blocks on lock B, waiting for Thread 1 to finish</p></li><li><p>Both threads are now waiting for the other, but neither thread can finish</p></li></ul><p>Congratulations! We just made a deadlock. And these types of deadlocks are really easy to create accidentally. In the above example, we were doing some work, but found we needed more locks than we originally had. When we tried to get them, we ended up getting blocked, forever.</p><h3>Common Deadlock Patterns</h3><p>Commonly, this type of deadlock I&#8217;ve ran into when dealing with typical Object Oriented Programming (OOP) and/or concurrent data structures. In OOP, there&#8217;s a very heavy emphasis on <em>encapsulation</em> where as much as possible is private. Often, this includes implementation details like locks. In OOP, there&#8217;s also this idea of reuse through interfaces and hierarchies. So when we pass a class as a parameter, we don&#8217;t actually pass it as that specific class. We pass it as a higher-level abstraction with no relation to the implementation. Meaning, the code calling the class has no idea if the method will block or not.</p><p>This means that we could end up having one thread start in class WorkQueue and then end up calling into class TaskProcessor, all while another thread starts in TaskProcessor and calls WorkQueue. And if both TaskProcessor and WorkQueue have class-wide locks, then we&#8217;ve made a deadlock!</p><h3>Mitigations</h3><p>Fortunately, there are mitigation strategies. First, having a thread lock everything it will (or will possibly) need at the start of a critical section can go a long way. It prevents the &#8220;I have resource A, but now I need B&#8221; scenario we saw. That said, when locking everything needed, make sure it&#8217;s done in the same order for every thread (i.e. lock A then B everywhere)! Otherwise, you&#8217;ll still end up with deadlocks.</p><p>For one, since we&#8217;re locking up-front, that means we haven&#8217;t done any critical work yet. So, if we fail to get even one lock, we simply unlock everything, wait for a random period of time (that way we minimize the chance we have contention on our next retry), and then retry to lock. This prevents deadlock (some thread can now make progress), but we could end up in starvation if we aren&#8217;t careful (one thread is in a constant unlock-and-retry cycle).</p><p>If we want a strategy that works regardless of whether we lock up-front or as needed, then we need to adjust things a little more. We can introduce a &#8220;try-lock&#8221; method. A &#8220;try-lock&#8221; will try to acquire a lock <em>without blocking</em>. If it fails to lock, it will throw an error (or return an error code/false boolean - all depends on your coding pattern!). We can now use this to retry acquiring the inner lock while doing a back-off with a retry limit. Again, this approach will cause some tasks to fail, but it&#8217;s better than blocking the whole system.</p><p>Another strategy is to add a timeout to every lock. That way, if we hit a deadlock, the system will abort the wait, we&#8217;ll unlock, and end with a failure. Not ideal to fail a task, but at least the system continues moving forward. Though, keep in mind that these timeouts will need to be short enough to prevent the whole system from stalling (as we still have a lock while we&#8217;re waiting), all while being long enough to prevent false positives (i.e. waiting on a resource that will be freed if we waited just a little longer).</p><h2>Thrashing and Contention</h2><p>These last two are more performance-related rather than &#8220;end the world&#8221; related. Threading does not automatically make programs faster. And, throwing more threads at the problem only works up to a point. After that, it makes things worse.</p><p>For one, adding more threads causes more contention on locks. More contention causes more waits, more waits increases the chance for starvation. However, contention and starvation aren&#8217;t the only issues with more threads.</p><p>There&#8217;s also thrashing.</p><p>CPUs only see the state of the currently running thread. The operating system uses some special hardware interrupts to schedule a switch to another thread. Switching to another thread involves unloading the memory of the current thread (the instructions/code, the registers, the cache, etc.), saving it off to memory, and then loading the other thread from memory. This process also clears out pipelines, and requires re-warming the CPU cache. This whole process is called &#8220;context switching.&#8221;</p><p>Context switching isn&#8217;t bad, it&#8217;s just a necessary part of having threads. It&#8217;s also a very slow or costly experience, so it&#8217;s best to minimize it.</p><p>Unfortunately, the more threads there are trying to do stuff, the more the operating system needs to trigger context switches, which means the slower everything goes. You may have noticed this when you open way too many applications at once, and they&#8217;re all trying to do stuff. The OS tries it&#8217;s best to schedule each one to do it&#8217;s work, but the time it takes the hardware to switch causes slow downs.</p><p>Overloading the hardware with context switches is &#8220;thrashing.&#8221; We&#8217;re basically beating down the hardware with so many context switches that it&#8217;s unable to get anything done.</p><p>Fortunately, modern CPUs and OS&#8217;s are actually really good at handling a lot of threads, so having slightly too many threads won&#8217;t make a big noticeable difference for us. However, trying to spawn hundred or thousands of threads definitely will cause issues.</p><p>The general rule of thumb is to spawn 1 - 2 threads per logical core<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>. 1 thread if it&#8217;s not going to be blocked waiting for I/O very often, 2 if it will be (that way one thread can work while the other is loading from disk).</p><p>So, how do we know how many logical cores there are? By using <code>navigator.hardwareConcurrency</code> - which is a number that tells us how many cores there are.</p><p>That said, on the web we have a little more to worry about. We don&#8217;t really want to go too high above the logical core count for our entire site. So if we&#8217;re using a lot of <code>Worker</code> we need to keep in mind how many active tabs or contexts the user will have at once. Whereas if we&#8217;re using a global <code>SharedWorker</code> we need to keep in mind how many legacy versions of the worker will be on the user&#8217;s machine at once.</p><p>For the <code>Worker</code> scenario, we may need to scale back our worker count or limit how many iframes we embed. For the <code>SharedWorker</code> we may need to consider forced refreshes of the site if the code gets too stale.</p><h2>Wrap Up</h2><p>We looked at a lot of different edge cases and issues that arise from multi-threaded programming. They aren&#8217;t impossible to deal with - code has been dealing with them for decades now. But, it&#8217;s also good to get familiar with the concepts at a high-level prior to diving head first into more complex multi-threaded programming.</p><p>Next time will be more about code rather than theory, as I&#8217;ll be going into some more synchronization primitives and their inner workings. I&#8217;ll first revisit the Mutex in more depth, and we&#8217;ll create an async version that uses promises instead of blocking a thread. We&#8217;ll also add timeouts to it as well.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>For optimistic concurrency, it is very important that the calculation is side-effect free (e.g. doesn&#8217;t send emails or update the database record) as it will be ran repeatedly in a loop. Also, be wary with where it&#8217;s used. If optimistic concurrency is used where there are frequent writes to memory, then it could cause starvation as writes get continually stuck in recalculation loops!</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Rust cannot detect logic races at compile time, only the easier data race subset.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>For Rustaceans, think &#8220;a thread paniced while it held a lock.&#8221; Also, Rust doesn&#8217;t prevent deadlocks, and badly misplaced panics can cause deadlocks as it kills the <em>thread</em> and not the <em>process</em>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Logical cores include things like hyper-threaded cores (basically a &#8220;virtual&#8221; core) that software can use. Since it&#8217;s what our code can use, it&#8217;s what&#8217;s generally what&#8217;s preferred in the software layer.</p></div></div>]]></content:encoded></item><item><title><![CDATA[JavaScript Mutexes]]></title><description><![CDATA[Adding locks to our threads]]></description><link>https://matthewtolman.com/p/javascript-mutexes</link><guid isPermaLink="false">https://matthewtolman.com/p/javascript-mutexes</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Thu, 05 Feb 2026 01:21:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my previous post, I introduced shared memory to JavaScript workers. Sharing memory ended up introducing data races. We fixed them using atomics, but atomics can only get us so far. At some point, we&#8217;ll need more than just a simple add, subtract, load or exchange. We&#8217;ll need to lock for longer, more complex logic.</p><p>Mutexes (mutual exclusion locks) allow us to do just that. They&#8217;re one of the fundamental threading primitives in most languages. A mutex lets us &#8220;lock&#8221; on a memory location and then later unlock. And they&#8217;d let us synchronize entire blocks of code.</p><p>So, let&#8217;s just pull out a mutex and&#8230;</p><p>Um, JavaScript doesn&#8217;t have mutexes<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. Instead, they have three little functions: <code>Atomics.wait</code>, <code>Atomics.waitAsync</code>, and <code>Atomics.notify</code>. The <code>waitAsync</code> function will give a promise<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> so that it can be used in the main thread. The <code>wait</code> function just blocks, so it cannot be used in the main thread. For this article, I&#8217;m just going to focus on <code>wait</code> for simplicity. If you need to use <code>waitAsync</code>, you&#8217;ll just need to translate over to promises.</p><p>Okay, so we have functions to wait and notify on a memory address. But, those aren&#8217;t mutexes. So, how does this help us?</p><p>Well, it turns out that wait and notify form the foundation of a <em><a href="https://en.wikipedia.org/wiki/Futex">futex</a></em> - which is an even lower-level primitive that we can use to make a mutex. A futex lets us wait on an address if the address stores a specific value. It also lets us change the value at that address, and then signal to one or more waiters that the value has changed and they should stop waiting. It&#8217;s basically a way to wait on a condition and control the signal that the condition changed.</p><p>And this was the last piece we needed. With futexes and atomic operations, we can build up any sort of synchronization primitive - whether it be mutexes, semaphores, condition variables, barriers, wait groups, or anything else. They end up being really powerful - <a href="https://cis.temple.edu/~giorgio/cis307/readings/futex.pdf">but are also tricky to use</a>.</p><p>To make matters worse, JavaScript doesn&#8217;t really have good multi-threaded debuggers, sanitizers, or analysis tools. The whole &#8220;threading in JavaScript&#8221; is really new, which means developing our own primitives inside JavaScript will be pretty tricky.</p><p>So, we won&#8217;t develop them in JavaScript. We&#8217;ll develop them in a language with really good tooling for making your own primitives that people normally give you, and then translate the result into JavaScript.</p><p>Out of all the languages to do it in, the one I found is the best at having those types of tools is C. Why? Because every operating system provides futexes, and a lot of developers in C make their own libraries (including &#8220;standard&#8221; libraries), and a lot of developers in C make tools to improve C, and there&#8217;s a lot more papers about making these types of primitives that assume either C or C-style C++. So, it&#8217;s our best shot at the initial design and validation prior to a translation.</p><p>That said, I&#8217;m not going to use a lot of the scary features in C. I&#8217;m not going to use malloc or free or macros. I won&#8217;t use the Win32 API (just the linux libraries) and I won&#8217;t use inline assembly. I am going to use pointers, but those are just memory addresses (basically the index into our shared memory buffer that something lives). </p><p>So, let&#8217;s get started.</p><h2>Building our Mutex</h2><p>For this mutex, I did a lot of looking into a lot of implementations people put online<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. I did find some back and forth between the articles where some people were trying to show someone else&#8217;s implementation was slow or incorrect in a pathological edge case (like missing the lock some 2^64-1 times in a row). Eventually, I landed on a design with the following lock steps:</p><ul><li><p>A single integer is stored at the mutex address</p></li><li><p>The mutex address is what is passed to the lock/unlock functions</p></li><li><p>The integer starts at 0 which signifies &#8220;unlocked&#8221;</p></li><li><p>When locking, a compare exchange is done to try to turn from 0 to 1 (locked)</p></li><li><p>If the compare exchange succeeds, then the lock is obtained</p></li><li><p>Otherwise, we enter a loop</p></li><li><p>In the loop, we do a compare exchange to set the value to 2 (contested)</p></li><li><p>We then wait on the integer&#8217;s memory address until it changes from 2 to something else</p></li><li><p>Then we try to acquire the lock and loop again if needed</p></li></ul><p>For unlocking, I do the following:</p><ul><li><p>Subtract 1 from the atomic value. If I end with 0, then no one was waiting and I return</p></li><li><p>Otherwise, someone was waiting, so I signal that I&#8217;m done and wake up 1 thread</p></li></ul><p>The C code I ended up with looks like the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;eb5e24f5-3bd5-4fe2-8ef9-444f67998e63&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">#include &lt;linux/futex.h&gt;
#include &lt;sys/syscall.h&gt;
#include &lt;stdio.h&gt;
#include &lt;errno.h&gt;

typedef int Mutex;

void futex_wait(int* addr, int waitWhen);
void futex_wake(int* addr, int amount);
int compare_exchange(int* addr, int* expected, int desired);
int fetch_sub(int* addr, int val);
void store(int* addr, int val);

// GCC, linux
void lock(Mutex* mux) {
    int cur = 0;

    // do a compare exchange (update if the value at the address matches our expected)
    // cur is our "expected" value
    // this always sets cur to what was at the address
    // it returns true if the exchange happened
    if (compare_exchange(mux, &amp;cur, 1)) {
       // we got the lock!
       return
    }

    do {
        // indicate contention
        if (cur != 2) {
            compare_exchange(mux, &amp;cur, 2);
        }

        futex_wait(mux, 2);
        cur = 0;

    // if we had contention, assume there are other threads when we lock
    } while (!compare_exchange(mux, &amp;cur, 2);
    // we got the lock
}

void unlock(Mutex* mux) {
    if (fetch_sub(mux, 1) != 1) {
        // always indicate we're unlocked
        // contended threads will indicate that there is contention
        store(mux, 0);

        futex_wake(mux, 1); // only wake 1 thread
    }
}

void futex_wait(int* addr, int waitWhen) {
    // we put it in a loop since the OS may cause spurious wake ups
    do {
        if (syscall(SYS_futex, addr, FUTEX_WAIT, 2, 0, 0, 0) == -1) {
            if (errno == EAGAIN) return; // value changed, no wait needed
            perror(); abort(); // something went wrong!
        }
    } while (__atomic_load_n(addr, __ATOMIC_SEQ_CST) == waitWhen);
}

int compare_exchange() {
    return __atomic_compare_exchange_n(
        mux,
        &amp;cur,
        1,
        false,
        __ATOMIC_SEQ_CST,
        __ATOMIC_SEQ_CST
    );
}

int fetch_sub(int* addr, int val) {
    return __atomic_fetch_sub(ptr, val, __ATOMIC_SEQ_CST);
}

void store(int* addr, int val) {
    __atomic_store_n(addr, val, __ATOMIC_SEQ_CST);
}

void futex_wake(int* addr, int amt) {
    if (syscall(SYS_futex, addr, FUTEX_WAKE, amt, 0, 0, 0) != -1) {
        perror(); abort(); // something went wrong
    }
}</code></pre></div><p>That&#8217;s, a lot of code. But most of it is fairly straightforward. We have some wrapper methods to hide verbose function names or functions with large parameter lists. We&#8217;re using a lot of atomics (which are wrapped), and we&#8217;re doing a futex wait/wake (wake being the C equivalent of notify).</p><p>The main takeaway is that we do 0 for unlocked, 1 for locked, and 2 for contended. We then have logic split between the locker and unlocker to maintain that differentiation.</p><p>As far as how it works, I&#8217;ve ran the above code (<a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/67444f1f2f271d773c6e78cf6edc1eae66bef67f/src/mutex.c">with some modifications for namespacing and additional features like timeouts</a>) thousands of times on multiple machines, both with and without thread sensitization turned on. That&#8217;s not to say it&#8217;s &#8220;perfect&#8221; or &#8220;bug-free&#8221; - only that it works well enough for me, so that&#8217;s what we&#8217;ll translate over.</p><h2>Translating to JavaScript</h2><p>Translating the C code isn&#8217;t all that difficult. Our pointers do become a &#8220;memory array + offset&#8221;, and we use the Atomics methods rather than syscalls. It&#8217;s fairly straightforward, though there are some differences in the C and JavaScript atomic APIs. That said, it&#8217;s not all that complicated.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;763fff22-6aca-480d-95bd-a3187ea3578d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">function mutex(memory, offset) {
    return {
        lock: () =&gt; {
            let expected = 0

            let cur = Atomics.compareExchange(memory, offset, expected, 1)
            if (cur === expected) {
                // got the lock
                return
            }

            while(true) {
                if (cur !== 2) {
                    expected = cur
                    Atomics.compareExchange(memory, offset, expected, 2)
                }

                Atomics.wait(memory, offset, 2)

                expected = 0
                cur = Atomics.compareExchange(memory, offset, expected, 2)

                if (cur === 0) {
                    // got the lock
                    return
                }
            }
        },
        unlock: () =&gt; {
            if (Atomics.sub(memory, offset, 1) !== 1) {
                Atomics.store(memory, offset, 0)
                Atomics.notify(memory, offset, 1)
            }
        }
    }
}</code></pre></div><p>The biggest difference now is that instead of passing only one memory address to our workers, we need to pass two: one for the working data, and another for the mutex location. Our worker code now looks like the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;395f80c9-41f0-4576-b98a-2b9f3b396232&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">importScripts('mutex.js') // load our mutex code

let memory = new Int32Array(new SharedArrayBuffer(1024))
let offset = 0
let muxOffset = 1 // must be different than offset!

onmessage = (e) =&gt; {
    if (e.data.type === 'init') {
        memory = new Int32Array(e.data.memory) // use the memory buffer were given
        offset = e.data.offset
        muxOffset = e.data.muxOffset
        // don't respond
    }
    else if (e.data.type === 'run') {
        const mux = mutex(memory, muxOffset)
        let lastRead = 0
        for (let i = 0; i &lt; 200; ++i) {
            mux.lock()
            try {
                memory.set([memory.at(offset) + 1], offset)
                // we can do more instructions now!
                lastRead = memory.at(offset)
            } finally {
                // unlock (in finally so we always run it)
                mux.unlock()
            }
        }
        postMessage({final: lastRead})
    }
}</code></pre></div><p>Our worker has been updated! Time to update our runner so we pass both offsets.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;61dcc8c1-8c42-47f0-85c4-3cc3ac108c92&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">/// our config
const memory = new SharedArrayBuffer(1024)
const arr = new Int32Array(memory)
const muxOffset = 1
const offset = 2
const numRunners = 4

const runners = []

for (let i = 0; i &lt; numRunners; ++i) {
    runners.push(run(memory, muxOffset, offset))
}

await Promise.all(runners)

console.log("Memory data: ", arr.at(offset))

function run(memory, muxOffset, offset) {
    const worker = new Worker('mutex-worker.js')
    const wait = new Promise((resolve) =&gt; {
        worker.onmessage = (e) =&gt; {
            resolve(e.data)
        }
    })
    worker.postMessage({type: 'init', offset, muxOffset, memory})
    return wait
}</code></pre></div><p>If we run that code, we see our expected final result of 800!</p><p>Of course, there is more to multi-threading. However, this should be enough to get you started. You&#8217;ve seen how to synchronize, and how to take C code and convert it over to JavaScript. With this, you should be able to turn my other C threading primitives (<a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/67444f1f2f271d773c6e78cf6edc1eae66bef67f/src/semaphore.c">semaphores</a>, <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/67444f1f2f271d773c6e78cf6edc1eae66bef67f/src/cond_variable.c">condition variables</a>, <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/67444f1f2f271d773c6e78cf6edc1eae66bef67f/src/barrier.c">barriers</a>) over to JavaScript.</p><p>When I next post about JavaScript threads, I&#8217;ll talk about some of the other edge cases you&#8217;ll hit (deadlocks, starvation, and logic race conditions).</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>They do have the Web Locks API - but that&#8217;s more for Indexed DB and Local Storage locks. It&#8217;s based around string names rather than addresses in a shared array buffer. Not saying you can&#8217;t use it - ever hammer can be used not-as-intended. Just that it&#8217;s outside the topic of <em>this</em> series as we&#8217;re focused on the APIs intended for workers and shared memory - and those APIs are the <code>Atomics</code> interface added with everything else.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>It actually gives an object with two fields, a boolean indicating if there is a promise and a promise. This is due to wait only waiting conditionally, more on that later.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p><a href="https://cis.temple.edu/~giorgio/cis307/readings/futex.pdf">Futexes are Tricky</a>; <a href="https://eli.thegreenplace.net/2018/basics-of-futexes/">Basics of futexes</a>; <a href="https://web.archive.org/web/20190213215555/http://locklessinc.com/articles/mutex_cv_futex/">Mutexes and Condition Variables using Futexes</a>; <a href="https://www.collabora.com/news-and-blog/blog/2022/02/08/landing-a-new-syscall-part-what-is-futex/">Landing a new syscall: What is futex?</a> and so many more that I lost the links to</p></div></div>]]></content:encoded></item><item><title><![CDATA[Sharing Memory Across Threads in JavaScript]]></title><description><![CDATA[Enter the races]]></description><link>https://matthewtolman.com/p/sharing-memory-across-threads-in</link><guid isPermaLink="false">https://matthewtolman.com/p/sharing-memory-across-threads-in</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Tue, 03 Feb 2026 18:27:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously I wrote about <a href="https://matthewtolman.com/p/sharing-threads-in-javascript">sharing threads across tabs</a>. Now we&#8217;ll talk about sharing memory across threads.</p><p>Which is harder to get setup than simply using the API. It turns out, there are little hardware vulnerabilities called <a href="https://meltdownattack.com/">Spectre and Meltdown</a>, and these rely on some strange hardware behavior when it comes to timing, threads, and shared memory. To mitigate this, shared memory in JavaScript contexts have <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements">security restrictions</a> that your site must meet.</p><h2>Making Shared Memory</h2><p>The requirements is that your site must be in a <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts">secure context</a> (i.e. localhost or HTTPS) and your site must be <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated">cross origin isolated</a>. Cross origin isolated basically means that you&#8217;re page is only pulling resources from locations allowed in CORS, and that popups are only allowed for the same origin, and embedding is restricted as well. In other words, CORS has to be setup properly, and it needs to be at least somewhat restricted.</p><p>In my experience, this has usually meant that I can&#8217;t just use <code>python3 -m http.server</code> to run my shared memory code as it doesn&#8217;t setup CORS properly. Instead, I need to create a test server with proper CORS headers.</p><p>To check if you&#8217;re cross origin isolated, read the boolean value <code>window.crossOriginIsolated</code>. If it&#8217;s true, you&#8217;re good.</p><p>Once those security requirements are met, it&#8217;s time to actually share memory.</p><p>To share memory, we first create a region of linear memory<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> to share. Then we send that memory off to the worker (either <code>Worker</code> or <code>SharedWorker</code>) for use. Once the memory is shared, we can use it to communicate data between threads, just like we would in threaded programming languages like Java and C++. To create shared memory, we simply create a <code>SharedArrayBuffer</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;9e7ba320-d5e9-4356-a4dc-065ea24176c6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">const sab = new SharedArrayBuffer(1024); // 1KB
worker.postMessage(sab); // share it with the worker</code></pre></div><p>Simple enough!</p><h2>Using Array Buffers</h2><p>So, now we need to create our workers that use the shared memory. Since multi-threading itself can be difficult, we&#8217;ll start with a worker that begins in an isolated, non-multi-threaded state which we can then &#8220;upgrade&#8221; to a multi-threaded state. That way we can separate shared memory issues from logic or implementation issues. Which will help a lot.</p><p>One of the first issues that we&#8217;ll run into with the linear memory approach is that array buffers (shared or non-shared) cannot be accessed directly. Instead, they need to be wrapped inside a typed array to &#8220;view&#8221; into the buffer. I don&#8217;t know why JavaScript did things this way, but that&#8217;s the way the standard went.</p><p>We&#8217;ll start with basic worker that just increments an index some number of times. Here&#8217;s our code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;10b4f2b6-5428-44b2-86d2-da8bcdfebbff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">let memory = new Int32Array(new ArrayBuffer(1024))
let offset = 0

onmessage = (e) =&gt; {
    for (let i = 0; i &lt; 200; ++i) {
        memory.set([memory.at(offset) + 1], offset)
    }
    postMessage({final: memory.at(offset)})
}</code></pre></div><p>Here we create an array buffer, and we get an integer array view over that buffer<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. We then increment that index in the buffer 200 times, and then we return the final measurement at that buffer.</p><p>We can easily test our code by spinning up a worker and sending a message, like so:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;b0b47e9a-8df9-4412-af97-745feea2ccf5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">const worker = new Worker('worker-01.js')
worker.onmessage = (e) =&gt; console.log(e.data)
worker.postMessage('run')</code></pre></div><p>With that, we get back our <code>{final: 200}</code> just as expected. If we post another message, we&#8217;ll get <code>{final: 400}</code> and so on.</p><p>Nothing too surprising. Now, let&#8217;s update our worker to use a shared memory buffer. We&#8217;ll need to update our message receiving. While we&#8217;re in there, we&#8217;ll also pass in the offset we&#8217;re writing to. Here&#8217;s the new worker code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;9cc34ae5-355e-42e5-b33c-aed78b756d3e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">let memory = new Int32Array(new ArrayBuffer(1024))
let offset = 0

onmessage = (e) =&gt; {
    if (e.data.type === 'init') {
        memory = new Int32Array(e.data.memory) // use the memory buffer were given
        offset = e.data.offset
        // don't respond
    }
    else if (e.data.type === 'run') {
        for (let i = 0; i &lt; 200; ++i) {
            memory.set([memory.at(offset) + 1], offset)
        }
        postMessage({final: memory.at(offset)})
    }
}</code></pre></div><p>Now, setting up our worker is a little more complicated, but not much. We&#8217;ll create a shared array buffer this time, and then we&#8217;ll send that to our worker and run it. We can then also wait for a response, and then read from our memory to make sure the values line up. Here&#8217;s our runner code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;bb1117a8-fcf6-4a76-a7db-41aae900ae2b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// our config
const memory = new SharedArrayBuffer(1024)
const arr = new Int32Array(memory)
const offset = 1

await run(memory, offset)
console.log("Memory data: ", arr.at(offset))

function run(memory, offset) {
    const worker = new Worker('example-02.js')
    const wait = new Promise((resolve) =&gt; {
        worker.onmessage = (e) =&gt; {
            resolve(e.data)
        }
    })
    worker.postMessage({type: 'init', offset, memory})
    return wait
}</code></pre></div><p>We get the same response back from the worker, but this time we can also read the data directly. Once we read the data, we see that we indeed have 200 in our main thread&#8217;s memory!</p><blockquote><p>I am aware that the top-level await will require the method to be in an async function in most JavaScript/TypeScript engines. I&#8217;m omitting the wrapper async function for brevity.</p></blockquote><h2>Racing with Multiple Threads</h2><p>Now that we have one thread writing to our data, let&#8217;s add more! We want all of them working towards the same goal (in this case, adding 200 to a piece of memory). It should be as simple as spawning more workers, and giving them each the same offset and memory, right? Let&#8217;s give it a try.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;895e595b-35d9-482c-b5e2-fad07c5c7b0e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">/// our config
const memory = new SharedArrayBuffer(1024)
const arr = new Int32Array(memory)
const offset = 1
const numRunners = 4

const runners = []

for (let i = 0; i &lt; numRunners; ++i) {
    runners.push(run(memory, offset))
}

await Promise.all(runners)

console.log("Memory data: ", arr.at(offset))

// omiting run function, same as above example</code></pre></div><p>Let&#8217;s run that and&#8230; we don&#8217;t get 800. At least, not always. In fact, we pretty much get different results in all of our test runs. For one of my runs, I got 688 total. When I looked at the messages from each of my runners for that run, I got 288, 288, 488, and 688. When I ran it again, I got 284, 303, 503, and 703. What&#8217;s going on?</p><p>Well, we introduced a <a href="https://en.wikipedia.org/wiki/Race_condition">race condition</a> into our code. All of our threads are trying to read and write the same memory at the same time. These reads and writes don&#8217;t have any sort of sequencing guarantees, so they get interleaved arbitrarily which causes the odd data. To resolve this, we must instruct the code on how to sequence memory access.</p><h2>Sequencing Shared Memory</h2><p>The simplest method to sequence memory is with <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics">atomics</a>. Atomics allow a single operation on memory (read, set, exchange, add, subtract) to be done in a single, sequenced operation. However, the catch is that <em>only</em> the atomic operation is sequenced. If we have two atomic operations, then they are sequenced <em>separately</em>, meaning that interleaving (and thus data races) can still happen. This means that the following code still has the same bug as before, even though it uses atomics:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;b93ba1b3-3036-4954-901d-6b0b943f9ef5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">let memory = new Int32Array(new SharedArrayBuffer(1024))
let offset = 0

onmessage = (e) =&gt; {
    if (e.data.type === 'init') {
        memory = new Int32Array(e.data.memory) // use the memory buffer were given
        offset = e.data.offset
        // don't respond
    }
    else if (e.data.type === 'run') {
        for (let i = 0; i &lt; 200; ++i) {
            Atomics.exchange(memory, offset, Atomics.load(memory, offset) + 1)
        }
        postMessage({final: memory.at(offset)})
    }
}</code></pre></div><p>Between the load and exchange, the CPU can interleave other instructions from other threads, which isn&#8217;t what we desire. To fix this, we need to have the <em>entire</em> operation happen in one atomic operation. This can be done as follows:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;64bf4dfe-9156-4a1c-8b69-74f0a5752f21&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">let memory = new Int32Array(new SharedArrayBuffer(1024))
let offset = 0

onmessage = (e) =&gt; {
    if (e.data.type === 'init') {
        memory = new Int32Array(e.data.memory) // use the memory buffer were given
        offset = e.data.offset
        // don't respond
    }
    else if (e.data.type === 'run') {
        for (let i = 0; i &lt; 200; ++i) {
            Atomics.add(memory, offset, 1)
        }
        postMessage({final: Atomics.load(offset)})
    }
}</code></pre></div><p>Now if we run this updated threading code, we&#8217;ll get 800 as our final result. We can run it several times and we&#8217;ll always end up with 800 in the end.</p><p>There&#8217;s a lot more to threading than what we&#8217;ve covered in this article. This at least should be enough to get some wheels turning.</p><p>In future posts, I&#8217;ll cover locking for more complicated synchronization, growing shared memory, and more. JavaScript doesn&#8217;t include locking primitives most developers are familiar with (outside of atomics), so we&#8217;ll need to build our own locking primitives, such as mutexes and semaphores.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Linear memory is basically an array of bytes. There&#8217;s a starting index (0) and an ending index (the maximum size of the memory). Most low-level systems and embedded code view memory this way since it&#8217;s closely mirrors the way the hardware operates. Memory allocators will then take this linear memory and subdivide it into &#8220;allocations&#8221; - which are essentially just pieces of memory designated for use. Allocations are pretty much just the allocator saying &#8220;this chunk of memory is already used.&#8221; When the allocated memory is no longer needed, it is &#8220;freed&#8221; by simply marking that section as &#8220;available.&#8221; When memory is &#8220;freed&#8221; we don&#8217;t destroy it, we simply mark it as available for reuse.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>The array interface provides <code>at</code> and <code>set</code> to access the memory, so that&#8217;s what we use. The set method does take an <code>ArrayLike</code> for the first parameter since it lets setting an array of values at once. We&#8217;re just setting one element, so we pass an array of one element.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Sharing Threads in JavaScript]]></title><description><![CDATA[Previously I wrote about the basics of threading in JavaScript. In short, threads are the worker specification, and you can pass messages to workers and receive messages from workers.]]></description><link>https://matthewtolman.com/p/sharing-threads-in-javascript</link><guid isPermaLink="false">https://matthewtolman.com/p/sharing-threads-in-javascript</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Mon, 02 Feb 2026 22:14:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously I wrote about the <a href="https://matthewtolman.com/p/playing-with-threads-in-javascript">basics of threading in JavaScript</a>. In short, threads are the worker specification, and you can pass messages to workers and receive messages from workers.</p><p>In the previous article, we saw how we could spin up a thread and use it to do some job outside of the main event loop. Which is perfect for most use cases!</p><p>Except for when it isn&#8217;t.</p><p>Most users don&#8217;t just have one tab open, they have many tabs open. And they may have many tabs open for the same site. This is especially true for sites where there&#8217;s a lot of data that users need to compare, correct, and enter. Often users will have two (or more) tabs open side-by-side so they can compare and contrast data, or copy data over.</p><p>With our previous example, each tab gets its own background thread - completely isolated from all other tabs. Which means if a user has two tabs open, then they have two additional background threads, and if they have 10 tabs open then they have 10 additional background threads.</p><p>The problem compounds once we start having more threads. It turns out, workers can spawn other workers which in turn creates more threads, and those workers can spawn even more. If we have such a structure, then the extra resource consumption can start to cascade.</p><p>Of course, modern browsers have some safeguards to control the impact. But, we don&#8217;t want to be wasteful developers here, especially if most of our threads are idle a majority of the time (like in my example where I offload long-running user interactions to a background process). Instead, we&#8217;d like a way to say &#8220;browser, give me up to this many threads for my <em>site</em> and share those threads across tabs.&#8221;</p><p>Fortunately, we can have threads be shared across tabs simply by using <code>SharedWorker</code> instead of <code>Worker</code>.</p><p>Well, almost. Since <code>SharedWorker</code> is shared across tabs, the browser needs to know which tab is sending or receiving data. This is done with <em>ports</em>. Ports simply indicate which thread is doing the communicating. As a result, every <code>postMessage</code> and <code>onmessage</code> must be tied to a port - on both the worker and page side. If we tried to access <code>postMessage</code> or <code>onmessage</code> directly, we would get a &#8220;function undefined&#8221; error.</p><p>So, with that background knowledge done, let&#8217;s make a shared worker!</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;5a11b3ba-1a77-4942-9cdc-cf9610030144&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">importScripts('calcs.js')

let jobCount = 0

onconnect = (e) =&gt; {
    const port = e.ports[0]
    port.onmessage = e =&gt; {
        ++jobCount
        const job = e.data.job
        const jobId = e.data.jobId
        if (job.hasOwnProperty('factorialOf')) {
            return sendResponse(port, jobId, factorial(job.factorialOf))
        }
        if (job.hasOwnProperty('fibonacciOf')) {
            return sendResponse(port, jobId, fibonacci(job.fibonacciOf))
        }
         sendError(port, jobId, "bad job type")
    }
}


function sendResponse(port, jobId, res) {
    port.postMessage({jobId, res, jobCount})
}

function sendError(port, jobId, err) {
    port.postMessage({jobId, err, jobCount})
}</code></pre></div><p>And now let&#8217;s use it!</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;bb70db2f-4406-4af3-b78b-f19ea0e14694&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">const sharedBackgroundThread = new SharedWorker('shared-background-process.js')
const workQueue = {}

function enqueueSharedJob(job) {
&#9;const jobId = crypto.randomUUID ? crypto.randomUUID() : ++incId
&#9;const ret = new Promise((resolve, reject) =&gt; {
&#9;&#9;workQueue[jobId] = {
&#9;&#9;&#9;resolve,
&#9;&#9;&#9;reject
&#9;&#9;}
&#9;})
&#9;sharedBackgroundThread.port.postMessage({jobId, job})
&#9;return ret
}

sharedBackgroundThread.port.onmessage = e =&gt; {
    const jobId = e.data.jobId
    try {
        console.log('Shared worker job count', e.data.jobCount)
        if (typeof e.data.res !== "undefined") {
            workQueue[jobId].resolve(e.data.res)
        } else {
            workQueue[jobId].reject(e.data.err || 'BAD MESSAGE FORMAT')
        }
    } finally {
        delete workQueue[jobId]
    }
}


await enqueueSharedJob({factorial: 5})</code></pre></div><p>It has a few extra steps with the ports, but otherwise it&#8217;s not that much different than using a normal worker.</p><blockquote><p>One thing you may have noticed is that I didn&#8217;t include the code for the actual calculations. Instead, I have an <code>importScripts</code> call. The <code>importScripts</code> call will load another JavaScript file into the context of the worker. It&#8217;s how we can load libraries and reuse code between workers and pages. In this case, I put the code for the factorial and fibonacci calculations inside a different file which I&#8217;m then loading.</p></blockquote><p>There is one other advantage to using a shared worker over a normal worker, and that&#8217;s communication <em>across tabs</em>! We can store all the ports, and have events broadcast to every open tabs, or have a cache, or some sort of shared state.</p><p>For simplicity, we&#8217;re going to just add a cache. The cache is going to be a simple &#8220;for this function input, cache this value&#8221; type of cache. It won&#8217;t have cleanup or upper bounds on size, but it&#8217;ll do for a small example.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;12c7b926-dd21-48e3-ab66-8280a1a6fd61&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// calc.js
const cache = {
    factorial: {},
    fibonacci: {}
}

function fibonacci(n) {
    if (typeof cache.fibonacci[n] !== 'undefined') {
        return cache.fibonacci[n]
    }

    let n1 = 0n
    let n2 = 1n

    for (let i = 0; i &lt; n; ++i) {
        [n1, n2] = [n2, n1 + n2]
    }
    cache.fibonacci[n] = n1
    return n1
}

function factorial(n) {
    if (!(n instanceof BigInt)) {
        n = BigInt(n)
    }
    let original = n
    if (typeof cache.factorial[n] !== 'undefined') {
        return cache.factorial[n]
    }
    let res = 1n
    while (n &gt; 1n) {
        res *= n
        n -= 1n
    }
    if (!(original instanceof BigInt)) {
        cache.factorial[original] = res
    }
    return res
}</code></pre></div><p>The cache will then persist so long as our shared worker does, and we will be able to use any of the calculated values across our tabs.</p><p>But, how long does our shared worker last? Well, <a href="https://html.spec.whatwg.org/multipage/workers.html#the-worker's-lifetime">per the spec</a> it&#8217;s basically so long as there&#8217;s at least one context referring to that shared worker (page, iframe, window, etc.) with a grace period for page navigation that&#8217;s defined by the browser.</p><p>For us, it means that any data we store in a shared worker is not durable. For background jobs, that&#8217;s totally fine.</p><p>One other detail to keep in mind is that there&#8217;s a shared origin policy for shared workers. Basically, if a page gets loaded on your site, and another page has already made a shared worker, than the new page will use the existing shared worker - even if the script file has changed since the worker was first made.</p><p>This can make live updates more complicated, since it&#8217;s not as simple as &#8220;just logout and log back in&#8221; anymore. It&#8217;s &#8220;close all the tabs, wait 10 to 30 seconds, then reopen everything.&#8221;</p><p>That&#8217;s not to say there aren&#8217;t solutions - they&#8217;re just not elegant. We just need to have a version in the script.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;beb59055-f6af-4b8b-88a9-0ece154d1db7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// old-page.html
const worker = new SharedWorker('worker-script-v1.0.0.js');

// new-page.html
const worker = new SharedWorker('worker-script-v1.1.0.js');</code></pre></div><p>All this to say, shared workers offer some really cool stuff, but due to their shared nature they&#8217;re a little trickier to get right.</p><p>Of course, we&#8217;re not done yet. There&#8217;s still more to go when it comes to JavaScript threads. I&#8217;ll post more soon.</p>]]></content:encoded></item><item><title><![CDATA[Archives of my old blog now online!]]></title><description><![CDATA[Read what I wrote years ago]]></description><link>https://matthewtolman.com/p/archives-of-my-old-blog-now-online</link><guid isPermaLink="false">https://matthewtolman.com/p/archives-of-my-old-blog-now-online</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Wed, 28 Jan 2026 23:29:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently (this year) I switched from my own self-hosted blogging solution to Substack. As part of my transition, I was not able to bring my blog history with me. Mostly since I used a custom blog engine and Substack doesn&#8217;t know how to import blog posts from that engine (and they shouldn&#8217;t have to - expecting them to know how every blog engine made by every random person isn&#8217;t practical. In fact, I&#8217;m quite impressed that they&#8217;re able to import blogs from just <em>some</em> engines!).</p><p>I didn&#8217;t manually bring my posts over either since I couldn&#8217;t find a way for Substack to release posts in the past or even with a publish date in the past (if someone knows, please let me know!). So, my solution was to make a dump of the raw, final HTML pages and then later get them up online under an &#8220;archives&#8221; site.</p><p>Well, they&#8217;re up now at <a href="https://archives.matthewtolman.com/">https://archives.matthewtolman.com/</a>! Anyone can read my old posts now. Sadly, there isn&#8217;t the ability for people to comment on the archives, but you can comment on this post. Have fun!</p>]]></content:encoded></item><item><title><![CDATA[Playing with threads in JavaScript]]></title><description><![CDATA[So, lately I&#8217;ve been doing a lot of multi-threading in C.]]></description><link>https://matthewtolman.com/p/playing-with-threads-in-javascript</link><guid isPermaLink="false">https://matthewtolman.com/p/playing-with-threads-in-javascript</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Wed, 28 Jan 2026 22:35:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So, lately I&#8217;ve been doing a lot of multi-threading in C. Which has been a lot of fun. But, I also know that there&#8217;s a way to do it with JavaScript using workers.</p><p>I&#8217;ve used service workers in the past to do some caching and pre-emptive reloading. When I used it in the past, it cut load times of our worst pages by half and made other pages feel instantaneous. However, service workers are meant more as a caching layer/custom proxy rather than as a way of doing general computations. It&#8217;s definitely not a thread I&#8217;d want to clog up in a modern web app given that they make so many network calls.</p><p>But, that still doesn&#8217;t mean that doing some sort of threading wouldn&#8217;t be useful. I&#8217;ve had a lot of pages get bogged down because there&#8217;s a large computation done in the main thread - a big no-no for anyone familiar with desktop development.</p><p>The problem with JavaScript is that it&#8217;s a notoriously single-threaded environment where all the code you write is in that main thread - or is it?</p><p>Well, surprise surprise, someone on the committee realized only having one thread was a bad idea, so they created something called &#8220;workers&#8221; which are basically background threads you pass messages to and receive messages from. Really handy.</p><p>The model is less the &#8220;C/Java&#8221; model where memory is shared (with some very limited exceptions) and much more the Erlang/Elixir model of message passing. Though it&#8217;s also pretty low level and doesn&#8217;t come with a good protocol for &#8220;awaiting&#8221; an event you send off. That said, it&#8217;s not too difficult to create a very crude protocol, and there are some really polished libraries out there.</p><p>For my simple use case, I went with just doing a simple protocol where I auto-assign ids, create a promise, and then put that promise&#8217;s resolve and reject methods in a map, followed by returning the promise. Then when I send a response back from my background thread, I include the same id as well as the data (or error), and then I lookup the corresponding resolve/reject method and call it to do a dispatch.</p><p>The reason for needing a map is simply because we only have a global &#8220;onmessage&#8221; handler which doesn&#8217;t have the context of what we just sent. So, we need a way to tell that handler where the resolver or rejector is so it can forward things along.</p><p>Here&#8217;s the little snippet of code I use:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;1d647c03-ef93-44cf-a6e0-575238ab0e58&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">let incId = 0
const backgroundThread = new Worker('background-process.js')

const workQueue = {}

backgroundThread.onmessage = e =&gt; {
&#9;const jobId = e.data.jobId
&#9;try {
&#9;&#9;console.log('Page worker job count', e.data.jobCount)
&#9;&#9;if (typeof e.data.res !== "undefined") {
&#9;&#9;&#9;workQueue[jobId].resolve(e.data.res)
&#9;&#9;} else {
&#9;&#9;&#9;workQueue[jobId].reject(e.data.err || 'BAD MESSAGE FORMAT')
&#9;&#9;}
&#9;}
&#9;finally {
&#9;&#9;delete workQueue[jobId]
&#9;}
}

function asyncJob(job) {
&#9;const jobId = crypto.randomUUID()
&#9;const ret = new Promise((resolve, reject) =&gt; {
&#9;&#9;workQueue[jobId] = {
&#9;&#9;&#9;resolve,
&#9;&#9;&#9;reject
&#9;&#9;}
&#9;})
&#9;backgroundThread.postMessage({jobId, job})
&#9;return ret
}</code></pre></div><p>And just like that, we have a really easy way to send jobs to our background thread and get a response back. In our code, it will look like the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;1e39d426-e791-42e9-81ca-f0e12daad1ad&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">async function doCalc(n) {
  return await asyncJob({expensiveCalculation: n})
}</code></pre></div><p>Of course, every protocol has two sides. So, let&#8217;s look at the other side: the background thread.</p><p>For this, we also have only a global &#8220;onmessage&#8221; handler. However, we don&#8217;t have to coordinate the state with some other context&#8217;s data, so we can pretty much do everything in the handler. Here&#8217;s my handler code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;4fd71993-2142-4ca0-905f-b9d8b1f1255d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">let jobCount = 0

onmessage = e =&gt; {
    ++jobCount
    const job = e.data.job
    const jobId = e.data.jobId
    if (job.hasOwnProperty('expensiveCalculation')) {
        return sendResponse(jobId, Math.pow(13, job.expensiveCalculation))
    }
    sendError(jobId, "bad job type")
}

function sendResponse(jobId, res) {
    postMessage({jobId, res, jobCount})
}

function sendError(jobId, err) {
    postMessage({jobId, err, jobCount})
}</code></pre></div><p>This preserves the job id, does the calculation, and sends it back. Not too bad.</p><p><a href="https://matthewtolman.dev/demos/workers/index.html">There is a demo I have online which shows this off</a>, as well as shared workers (very different from service workers) which I&#8217;ll talk more about in another post. To help illustrate the lack of stuttering, I have an animation that plays in the JS loop with an update every frame (it&#8217;s just a really small square bouncing animation). There&#8217;s also a button that runs everything in the main thread instead of the background thread for compare/contrast. I also use something a little more expensive (fibonacci + factorial of large numbers with BigInt) to make the stutter more noticeable on the main thread example.</p>]]></content:encoded></item><item><title><![CDATA[Property Testing in C]]></title><description><![CDATA[A poor-man's fuzzer]]></description><link>https://matthewtolman.com/p/property-testing-in-c</link><guid isPermaLink="false">https://matthewtolman.com/p/property-testing-in-c</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Mon, 12 Jan 2026 19:20:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!uqu-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last time I wrote about <a href="https://matthewtolman.com/p/snapshot-tests-in-c">snapshot testing</a>, which is great for capturing behavior of a current system - especially in a way that can be quickly checked. I use it as a complement to traditional example-based unit testing where we check a system gives expected (known) output to known input.</p><p>However, snapshot testing and example-based testing don&#8217;t find bugs. They&#8217;re not meant to. They instead act as anchor points for a system, saying that the system behaves this way at this point - but they say nothing about any other point.</p><p>For instance, let&#8217;s say we wrote a custom memory allocator, and we&#8217;re writing tests for it. We might write an example-based test like the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;269d2263-8879-430d-9937-c28477babc75&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">TEST(my_allocator) {
    int initUsage = my_alloc_usage();
    void *ptr = my_alloc(24);

    int curUsage = my_alloc_usage() - initUsage;
    int expectedUsage = 24;

    ASSERT_EQ(expectedUsage, curUsage);
    ASSERT_NE(NULL, ptr);

    my_free(&amp;ptr);

    int finalUsage = my_usage();
    ASSERT_EQ(initUsage, finalUsage);

    PASS();
}</code></pre></div><p>It&#8217;s not hard to add a single point. The above code shows a test that allocates and frees some amount of memory, with different assertions making sure things work. We can add more tests for different sizes, or maybe multiple allocations. Each test will act as an anchor checking how our allocator performs.</p><p>But, most bugs in production don&#8217;t happen at the anchored input - and many don&#8217;t happen close around the input. Instead, they tend to happen in the gaps between each anchor - gaps which tend to be very large and obscure to find. How the system really behaves is unknown until the code is ran with that input.</p><p>To illustrate how hard it is to get good coverage, let&#8217;s look at our use case. Users making sure that the allocator behaves a certain way way at that <em>input point</em> - but only at that input point. In our case, the input is the combination of:</p><ul><li><p>The order of allocations</p></li><li><p>The order of frees for each allocation</p></li><li><p>The size of each allocation</p></li><li><p>The order allocations and frees are interleaved</p></li></ul><p>It turns out, changing any one of those parameters can cause our allocator to run through any number of different code paths - and trigger any number of hidden bugs. </p><p>We could add a lot of test cases and try to cover as many possibilities by hand - but we would be typing test code until the sun burned out trying to catch every possible combination. And, most of the tests would look suspiciously similar.</p><p>Well, what if we wrote a program that tried to break our app? We could do that - it&#8217;s called a fuzzer. And there are several programs already out there for different languages. However, writing a fuzzer is non-trivial, and getting an existing fuzzer to work would require not only installing it on every dev machine (and build server), but also time to set it up, training on how to use it, and dealing with updates. For many projects, it&#8217;s worth the effort, but for many projects it&#8217;s also not worth the effort.</p><p>Is there an in-between? Something simple enough to write as part of tests but powerful enough to test many different cases? Well, yes!</p><p>They&#8217;re property tests!</p><p>Property tests are just a fancy name saying &#8220;with random data shaped like this, throw it into my code and expect it to sort of behave like that.&#8221; In the case of our allocator, we just take a bunch of random allocation and free requests and throw them at our code. Similar to the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;1d7a6ddf-ecd4-4236-b4f5-a018cbd461ab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">#ifndef MAXROUNDS
#define MAXROUNDS 1000
#endif

#ifndef MAXALLOCS
#define MAXALLOCS 100
#endif

TEST(my_allocator_rand) {
  size_t initUsage = my_alloc_usage(&amp;alloc);
  void *allocations[MAXALLOCS] = {NULL};
  srand(time(0));

  for (size_t round = 0; round &lt; MAXROUNDS; ++round) {
    size_t slot = (rand() / 16) % MAXALLOCS;
    if (allocations[slot]) {
      my_free(allocations[slot]);
      allocations[slot] = NULL;
    } else {
      size_t allocAmount = (rand() / 16) % 4000 + 64;
      allocations[slot] = my_alloc(allocAmount);
      ASSERT_NE(NULL, allocations[slot]);
    }
  }

  for (size_t slot = 0; slot &lt; MAXALLOCS; slot++) {
    if (allocations[slot]) {
      peaks_free(&amp;alloc, allocations[slot]);
    }
  }

  size_t finalUsage = my_alloc_usage(&amp;alloc);
  ASSERT_EQ(initUsage, finalUsage);

  PASS();
}</code></pre></div><p>The above code randomizes<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> the interleaving of allocations with frees and randomizes the allocation size. It&#8217;s a very simple check, and yet it covers a wide range of possibilities.</p><p>However, property tests aren&#8217;t a silver bullet - and there are some notable drawbacks. But before we get to those drawbacks, let&#8217;s show another example that will highlight the drawbacks even more. Let&#8217;s move from memory allocators to math functions - like sin. Here&#8217;s a test for sin.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;84ebf7a5-9812-4798-be6c-12e36be167fb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">#ifndef MAXROUNDS
#define MAXROUNDS 1000
#endif

TEST(my_sin) {
  void *allocations[MAXALLOCS] = {NULL};

  for (size_t round = 0; round &lt; MAXROUNDS; ++round) {
    double in = (double)(rand() / 16) / 50000.0;
    double out = my_sin(in);
    
    ASSERT_LE(out, 1);
    ASSERT_GE(out, -1);
  }

  PASS();
} </code></pre></div><p>If you&#8217;re wondering how this test ensures each output is the correct approximation for the corresponding input, the answer is very simple: it doesn&#8217;t. Which leads me into a big downside of property tests: they don&#8217;t really check for correctness.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><p>We&#8217;re running through a lot of questions - but we can&#8217;t verify if they&#8217;re right. So, what&#8217;s the point?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uqu-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uqu-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 424w, https://substackcdn.com/image/fetch/$s_!uqu-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 848w, https://substackcdn.com/image/fetch/$s_!uqu-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 1272w, https://substackcdn.com/image/fetch/$s_!uqu-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uqu-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png" width="1456" height="1048" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1048,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:221078,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://matthewtolman.com/i/184163390?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uqu-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 424w, https://substackcdn.com/image/fetch/$s_!uqu-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 848w, https://substackcdn.com/image/fetch/$s_!uqu-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 1272w, https://substackcdn.com/image/fetch/$s_!uqu-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F433f8723-5863-4201-90d5-ca17273b6cd9_1456x1048.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The point is we&#8217;re looking for <em>unknown</em> <em>failures</em> not <em>known</em> <em>successes</em>. This is most commonly manifested with exceptions (e.g. error codes), failed assertions (via <code>assert</code><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>), segmentation faults, and behavior detected by sanitizers (like clang&#8217;s thread sanitizer, address sanitizer, memory sanitizer, and UB sanitizer).</p><p>For instance, our free code may look something like the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;0d092734-c829-4e30-85ad-f4b0148c413d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">void my_free(void *ptr) {
  if (ptr == NULL) return;

  pthread_mutex_lock(&amp;global_alloc_lock);

  struct MyBlockHeader *block = my_align_pointer_down(
    (struct MyaBlockHeader *) ptr - 1, MY_MEM_ALIGNMENT);
  
  // My ensure macro runs an assertion in debug and release mode
  // On a failure, it outputs a formatted message
  // Along with file and line information to quickly id the failed code
  assert(block != NULL);

  // Ensure that the header is valid (e.g. look for "magic bytes")
  assert(my_block_is_valid(block));

  // Detect "double frees"
  assert(block-&gt;used == true);
  block-&gt;used = false; // mark it as freed

  merge_with_neighbors(block);

  // make sure we didn't corrupt the current (or neighboring) blocks
  assert(my_block_is_valid(block));
  assert(block-&gt;next == NULL || my_block_is_valid(block-&gt;next));
  assert(block-&gt;prev == NULL || my_block_is_valid(block-&gt;prev));
}</code></pre></div><p>These asserts add a lot more power to property tests. If we fail an assert, we&#8217;ll know. And since we&#8217;re doing a lot more tests and a lot more input variation, we make it a lot more likely that we&#8217;ll trip a bug which will fail an assert.</p><p>But wait - we&#8217;re using random numbers in our property tests! Which means that even if we find a bug we may not be able to reproduce it.</p><p>That is unless we do two things - print the seed at the start of every run, and provide a way to seed a run if we desire. Once we do that, we&#8217;re able to reproduce a failure<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a> and start debugging it.</p><p>There are other aspects to property testing that people tend to focus a whole lot on (rules for input generation, how to define &#8220;properties&#8221;, shrinking, etc.). However, I&#8217;ve found that they&#8217;re either very easy to do, not very important, or mostly used when trying to create a regression test (example or snapshot test) around a failed property test. In other words, they&#8217;re &#8220;nice to have&#8221; but aren&#8217;t the real meat of property testing, and can often distract or take away from the use of property testing.</p><p>If this post inspired you to start using property testing, I do have some library recommendations for you.</p><ul><li><p><a href="https://github.com/silentbicycle/theft">theft</a> is a C99 property testing library that&#8217;s been around for a while. It is pretty heavy-handed when it comes to </p></li><li><p><a href="https://github.com/emil-e/rapidcheck">rapdicheck</a> is a C++ property testing library which I&#8217;ve used to test C code</p></li><li><p><a href="https://gitlab.com/mtolman/test_jam">Test_Jam</a> is a C++/C library I made for property testing a few years back. The C interface is very primitive and poorly tested as I focused primarily on the C++ interface.</p></li><li><p><a href="https://git.matthewtolman.dev/mtolman/peaksc">PeaksC</a> is a library I&#8217;m currently working on which has some property testing utilities built in. I&#8217;m currently working on this project. It is highly opinionated though, so if you need a drop-in solution for a large code base, then one of the other solutions may be better.</p></li></ul><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://matthewtolman.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://matthewtolman.com/subscribe?"><span>Subscribe now</span></a></p><p></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Yes, I know rand() is a linear congruent generator and isn&#8217;t very &#8220;random&#8221; - but it works well enough for basic use cases and can be quickly switched out.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>A property test I can think of  which could verify sin is to generate a right triangle and solve for all the angles without using sin, and then verify that sin lines up with the solved solution. There are often cases like this, but they take a lot more working out to get right, and by the time you do when a test case fails it&#8217;s unclear if it&#8217;s a bug in the code or a bug in the test. Which makes debugging way more difficult.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>The two downsides to assert is it only runs in debug builds - meaning it&#8217;s not going to catch bugs in a release build - and it has very limited debug information when the program crashes. In my code, I have a macro called &#8220;ensure&#8221; which runs in debug and release builds, an which allows printing a formatted string that also includes the file and line number.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>There are some other caveats, like if a test is non-deterministic by nature (e.g. relies on calls to the system clock or doing network calls) then a seed won&#8217;t be sufficient to reproduce without additional work. But at that point, we&#8217;re leaving the realm of property tests and entering either mocking, deterministic simulation testing, or record-and-replay debugging.</p><p>Of which mocking is the worst option because we now exclude any possibility of finding unknown failures on a whole segment of our code - which also tends to be the most flaky and complex dependency of our code due to it&#8217;s non-deterministic nature. Couple this with the fact that mocked code is almost always someone else&#8217;s code (e.g. OS or network service) which also means we&#8217;re not testing our integration layer with somebody else&#8217;s code. Not only that, but we aren&#8217;t running mocked code - so we aren&#8217;t even testing our known successes in addition to not finding unknown failures!</p></div></div>]]></content:encoded></item><item><title><![CDATA[Snapshot Tests in C]]></title><description><![CDATA[Lately I&#8217;ve been working on a fairly big C library (it&#8217;s still early days, and I&#8217;ll write more about it over time).]]></description><link>https://matthewtolman.com/p/snapshot-tests-in-c</link><guid isPermaLink="false">https://matthewtolman.com/p/snapshot-tests-in-c</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Fri, 09 Jan 2026 02:44:43 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/de7d13c1-884a-42aa-8df4-c0dcca2aae4d_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Lately I&#8217;ve been working on a <a href="https://git.matthewtolman.dev/mtolman/peaksc">fairly big C library</a> (it&#8217;s still early days, and I&#8217;ll write more about it over time). As part of this library, I&#8217;ve been creating my own test framework.</p><p>Most of my tests are <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/7be02e2461d607ea48a608859f194ab39e1e52d6/test/pagealloc.c">typical unit tests</a> - you have some setup, run your code, and then do a series of assertions with hard-coded values. Nothing too surprising, and the test framework for that isn&#8217;t too difficult to make.</p><p>For most of my tests, that style is ideal. It&#8217;s very focused (both writing the tests and what is being tested), and it&#8217;s not too hard to setup. Also, since I&#8217;m writing the examples, it works really well when I know what the output should be (which is most of the time).</p><p>However, it does have a lot of drawbacks. I&#8217;m not going to cover all of the drawbacks, or how I&#8217;m trying to address each one. Rather, I&#8217;m going to focus on one singular drawback: I need to know the answer/behavior ahead of time.</p><p>Again, for most use cases, this is fairly trivial. Memory allocators have a very known behavior (allocates memory sufficiently large or returns an error, memory doesn&#8217;t overlap, memory isn&#8217;t leaked, etc.), so for my memory allocators it&#8217;s pretty easy to write some typical unit tests. Similar thing for most of the other code I write. I know what behavior I want it to have.</p><p>Where it&#8217;s not so true is when it comes to low-level math functions - like sin, cos, tan, etc. I <em>don&#8217;t</em> know what the answer should be for every number ahead of time. I&#8217;m not someone who regularly calculates sin by hand or has a book full of tan lookup tables sitting on my shelf.</p><p>This means that I need to lookup or calculate the correct values. Fortunately, the standard library provides many math functions, so I wrote something like the following:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;2b15eb71-e06c-4888-9f25-994048c9661a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">for (int i = 0; i &lt; numInputs; ++i) {
    double expected = sin(inputs[i]);
    double actual = mySin(inputs[i]);

    if (!approx_equal(expected, actual, 0.0000001)) {
        fail();
    }
}
pass();</code></pre></div><p>This works great - so long as you only stick with one standard library implementation. It turns out, there are different ways of implementing sin. A lot of open source standard libraries I&#8217;ve gone through appear to be based off of the <a href="https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_sin.c">Sun Microsystems code in FreeBSD</a> - which to be fair is what I&#8217;m basing most of my math code around too. This means that they&#8217;re very compatible in similar precision, error tolerances, and biases (sin methods are an <em>approximation</em> not a <em>calculation</em> so a lot can change due to different trade-offs made in the implementation).</p><p>However, not everything is open source, and Microsoft&#8217;s MSVC standard library notably is not. So much so that when I used FreeBSD&#8217;s approximation, my above test failed<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. Which, is honestly what I expected. The reason I wanted to have my own sin was simply because I want my code to behave the same way on every platform (or at least as close as I can get it). Having a math library that executes the same code on every platform is a huge step towards that determinism.</p><p>Fortunately, my laziness got the better of me and I didn&#8217;t want to manually generate a table of expected and actual values and try to keep that updated. I have that pattern in a few places in my code and it&#8217;s a nightmare to debug and maintain.</p><p>So, for a while I just wrapped whatever was in the standard, that way my simple test would pass on both open-source and closed-source libraries. In the meantime, I was thinking about how to write a test that better captured the FreeBSD-based version of sin I was wanting to have.</p><p>One day I read about snapshot tests and was reminded about <a href="https://blog.janestreet.com/the-joy-of-expect-tests/">Jane Street&#8217;s</a> blog post on their snapshot testing.</p><p>I had known about snapshot tests when I first wrote my library, but I&#8217;ve only used snapshot tests in the context of the React components, and React snapshot tests <em>suck</em>. I&#8217;m not going to go into too much detail in this post, but here are the main gripes I have with React snapshot tests:</p><ul><li><p>They check the <em>exact</em> HTML output, not the <em>visual</em> output and not the <em>semantic</em> output. This makes tests flaky in the worst way - they fail for changes you don&#8217;t care about and pass for changes you do care about</p></li><li><p>The snapshots are incredibly verbose, so when things do change the deltas can be massive - which means no one looks at them</p></li><li><p>Snapshots aren&#8217;t stored in the code, but rather in a separate directory. So now there are multiple files that you need to look at to understand what a test is doing</p></li></ul><p>Because of the issues I&#8217;ve had with React snapshots, I had initially written off snapshot testing <em>entirely</em>. And when I had first read Jane Street&#8217;s post I couldn&#8217;t comprehend how the mess of snapshot testing could be enjoyable.</p><p>And then, after pondering my own predicament, I realized it was precisely what I wanted.</p><p>I already had a way to generate correct cases -  simply test against GCC/Clang/Musl. All I needed was a way to capture that output and preserve it in the code. And snapshot tests let me do that.</p><p>So, I quickly wrote my own <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/7be02e2461d607ea48a608859f194ab39e1e52d6/include/testing.h#L403-L437">snapshot testing macro</a>. This macro was based around Jane Street&#8217;s methodology where it generates correct source code to embed rather than separate files. Though, for ease of use with how many cases I was going to be generating, I gave it the ability <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/7be02e2461d607ea48a608859f194ab39e1e52d6/src/testing.c">update the source files directly</a>, though I normally have it off by keeping a &#8220;return false&#8221; at the start. That way I only update the test cases when I want them to be updated, not all the time.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><p>So far it&#8217;s been really great! I have <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/7be02e2461d607ea48a608859f194ab39e1e52d6/test/math.c#L323-L456">a lot of snapshot tests</a> in my math code right now. This allows me to separate my math tests from the standard library implementation, which is needed for me to standardize the math functions I use across platforms.</p><p>That said, there are some limitations in my system. For one, my parser is really simple - it basically looks for the phrase &#8220;CHECK_SNAPSHOT&#8221; followed by an open parenthesis and then a comma (while ignoring commas inside quotes) in the C file. If you&#8217;ve ever used C macros, you&#8217;ll know this isn&#8217;t enough to handle complex use cases. Something simple like the following would break my snapshot test code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;5af686bf-2804-48ab-ae1f-8b894cd152d9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">#define COMMA ,

TEST(my_test) {
    CHECK_SNAPSHOT(&#8220;hello&#8221; COMMA &#8220;bye&#8221;);
}</code></pre></div><p>Also, it only looks for the first occurrence on a line - it doesn&#8217;t try to understand if it&#8217;s the &#8220;right&#8221; occurrence. So each snapshot check must be on a separate line.</p><p>Of course, getting that level of robustness takes a <em>lot</em> of work. I&#8217;d have to have a preprocessor, C compiler front-end, etc. just to identify what code to modify - something that doesn&#8217;t provide me benefit and only provides marginal benefit generally.</p><p>The bigger issue is that my snapshot testing requires values to be strings - which is a little annoying in C traditionally due to the fact memory is manually managed. Combine this with the fact I have a limited parser and now it&#8217;s a lot harder for someone to macro their way to &#8220;elegant&#8221; test code.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p><p>My solution is just to have a <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/7be02e2461d607ea48a608859f194ab39e1e52d6/test/math.c#L174">fixed buffer</a> that I reuse and to have <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/7be02e2461d607ea48a608859f194ab39e1e52d6/test/math.c#L197-L198">macros define how to write</a> to that buffer.</p><p>Overall, I&#8217;m really pleased with my snapshot test solution.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>The numbers are fairly similar, but they&#8217;re within a percentage value of each other rather than a fixed distance, and my approximate equality method only handled a fixed distance. I didn&#8217;t feel like trying to figure out what the value tolerance so just moved on to other options - especially since at the end of the day I&#8217;m wanting to match GCC/Clang/Musl since those are closer to the FreeBSD library anyways.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>At some point, this will become a CLI/environment variable option. However, I get the most bang for effort by simply using a return statement for now and then refactoring it later once my usage/needs gets more complex and I can know what features I really need.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>I do have a little trick where it will match a phrase with &#8220;CHECK_SNAPSHOT&#8221; in it rather than doing an exact match - but that may still be too limiting for some use cases. Also, I haven&#8217;t actually tested how it works with extended phrases, so it may not actually work.</p></div></div>]]></content:encoded></item><item><title><![CDATA[A build system around nix-shell]]></title><description><![CDATA[I finally got my CI/CD pipeline moved over to Forgejo actions. As part of my migration, I had to learn a lot about Forgejo (since it&#8217;s my first time using Foregejo), but I also needed to learn a lot about how a nixos host works with Forgejo too. Mostly since I&#8217;m still new to NixOS and there&#8217;s a fairly steep learning curve.]]></description><link>https://matthewtolman.com/p/a-build-system-around-nix-shell</link><guid isPermaLink="false">https://matthewtolman.com/p/a-build-system-around-nix-shell</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Thu, 18 Dec 2025 19:01:01 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zSn1!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf1be78b-421d-4c20-91ad-957de09d96fe_720x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I finally got my CI/CD pipeline moved over to <a href="https://git.matthewtolman.dev/mtolman/peaksc/actions">Forgejo actions</a>. As part of my migration, I had to learn a lot about Forgejo (since it&#8217;s  my first time using Foregejo), but I also needed to learn a lot about how a nixos host works with Forgejo too. Mostly since I&#8217;m still new to NixOS and there&#8217;s a fairly steep learning curve.</p><p>As such, nothing here is &#8220;idiomatic&#8221; nix or Forgejo. It&#8217;s just what I managed to cobble together after several days of trial and error (which you&#8217;ll see reflected in my action run history - there was a <em>lot</em> of trial and error).</p><p>That said, I did get something working, and I&#8217;m pretty happy with it and just wanted to share it briefly.</p><h2>Part 1: The Project</h2><p>The project I chose to work on first is my most recent C project, which is basically a collection of functions, macros, etc. for me to use in other C projects. It has it&#8217;s own unit test library, syscall wrappers, threading primitives, allocators, etc. In short, it&#8217;s trying to be my own &#8220;standard&#8221; library that I can use to minimize my reliance on other standard (or platform-specific) libraries. It also adds in additional functionality that I find standard libraries match.</p><p>I chose this project as a starting point for a few reasons. Namely:</p><ol><li><p>It uses C99 with no 3rd-party libraries outside of standard and OS-libraries (which it wraps), so it&#8217;s very simple to get a build environment setup.</p></li><li><p>I&#8217;ve done the most experiments with using nix-shell for setting up the build system in this project, so I already have a good starting point.</p></li><li><p>I&#8217;ve done a lot of tests to make sure that the code compiles with many different tool-chains. This means that if I run into any errors it&#8217;s most likely to be a configuration issue not a code issue.</p></li><li><p>I have minimal artifacts being produced right now (pretty much just documentation), which means I don&#8217;t have to setup an artifact repository quite yet. This helps narrow the scope of my experiment (and the work needed to get off the ground)</p></li><li><p>It&#8217;s a library not an application, so there&#8217;s no deploy pipeline to worry about which greatly reduces the scope of my experiment</p></li></ol><p>My experiments in this project included using nix-shell to spin up different versions of GCC and Clang, as well as cross-compile for Windows and Linux on different architectures - and run those cross-compiled versions in virtual machines. So, I had a pretty good starting point to say the least. Before we go too far into the CI/CD part, let&#8217;s first dive into my starting point a little more.</p><h3>Nix-Shell for local builds</h3><p>I had three parts<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> for my nix-shell builds:</p><ol><li><p>A <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/build.sh">build.sh</a> file that took in a lot of environment variables to tweak things as needed</p></li><li><p>A <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/branch/main/nix">folder of *.nix files</a> with instructions on how to run build.sh for every tool-chain and platform</p></li><li><p>A <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/branch/main/nix">nix-build.sh</a> file which ran through every one of my nix files in order and ran them</p></li></ol><p>The build.sh file was a little complicated - not because building my library is complicated (it&#8217;s pretty much just compiling my <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/branch/main/nix">peaksc.c</a> file which then includes all my other .c files). Rather, it&#8217;s because I have <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/build.sh#L97-L129">code generation</a> for my utility executables. This code generation handles things like <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/generators/xxd.c">turning text and data files into C-style byte arrays</a>, or <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/generators/data_structures.c">generating type-specialized data structures</a>. These utilities then have their outputs used for further steps, like running additional tests or even as a prerequisite for the other utilities.</p><p>What makes this more complex is that not every one of my tool chains can reliably run these executables - or at least not as part of a &#8220;build&#8221; step. <a href="https://emscripten.org/docs/porting/files/file_systems_overview.html">Emscripten has a lot of restrictions where it can&#8217;t directly access the disk but uses a virtual file system</a>, at least with default build settings (due to how WASM works and the need to &#8220;expose&#8221; disk access through a JS API). This means that generating code files doesn&#8217;t really work since changes would be lost.</p><p>I also run into issues with Wine and MinGW since MinGW and Wine don&#8217;t like being loaded as part of the same nix-shell (from what I can tell it has something to do with the &#8220;<a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/nix/mingw-shell.nix#L3">crossSystem</a>&#8221; config in my nix-shell file).</p><p>So, this means that my build.sh file needs to be able to not run the utilities. However, I still want to compile them to make sure that I didn&#8217;t break anything there (like use a linux-only API in a windows executable) - so I still need a way to get the generated files.</p><p>On my local machine, how I get those generated files is I simply run a non-emulated version of my generators <em>before</em> I run anything that can&#8217;t generate those files. That way they live on-disk already and I can just reuse them.</p><p>Overall, this system works great - although it&#8217;s really slow especially as configurations have grown a lot over time. It turns out running 9 different emulation layers (some for different versions of windows, others for different CPU architectures) and 12 non-emulated toolchains (including different sanitizers, older versions of compilers, and some esoteric compilers) just takes time. I am glad that I&#8217;m using C for doing this crazy experiment - trying to run all of these setups in C++ or Rust would be painfully slow!</p><p>The other benefit is that running just one tool-chain was really easy to do, just run &#8220;nix-shell nix/&lt;toolchain&gt;-shell.nix" and suddenly you had a reliable, reproducible run on that tool-chain - even if it needed emulation!</p><h2>Part 2: Plan vs Reality</h2><p>My initial plan was actually really simple. I have NixOS installed. I can simply setup a Forgejo runner with my host NixOS (I know it&#8217;s not &#8220;secure&#8221; - but this is self hosted for me with no one else able to contribute or trigger or login), and then I would just run my &#8220;nix-build.sh&#8221; script and be done. Simple, right?</p><p>Well, it would be, if there weren&#8217;t safety mechanisms built in where the runner actually ran in a virtual Nix environment and didn&#8217;t have access to the nix-shell command - which is what my entire local build system was built around. Argh.</p><p>Okay, new plan. I had two options:</p><ul><li><p>Rebuild my entire build system so that I have all the tools globally available and I just run those specific tools</p></li><li><p>Figure out how to get nix-shell to work</p></li></ul><p>Option 1 is what I&#8217;ve seen in most enterprise places I&#8217;ve worked at. The build system is setup fundamentally differently than the local environment. And it&#8217;s awful. Things work in the build that don&#8217;t work locally and vice versa. A true pain. Also, I don&#8217;t think it would be possible to do easily given that I have multiple versions of the <em>same</em> tool-chain. I don&#8217;t want to figure out how to keep gcc-9 and gcc-12 versions straight. With all of this, Option 1 felt like it wasn&#8217;t a real option - at least not for long-term success. So I crossed it off quickly.</p><p>So, Option 2 it is then. So, how do I get nix-shell to work?</p><p>Well, I tried a lot of things. Exposing programs, installing the &#8220;nix&#8221; package manager directly on the hidden VM, etc. Eventually, what I settled on was to not run on the host machine at all and instead run everything in a nixos docker container. That way it was a &#8220;clean&#8221; NixOS, not a nix VM in NixOS. Of course, the base nix container doesn&#8217;t quite have everything installed that Forgejo needs. Forgejo is built for NodeJS devs, and so they assume NodeJS is installed when they do a git checkout. This meant I had to update my run steps to <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/.forgejo/workflows/change.yml#L6">install NodeJS before I did a checkout</a>. Also, docker runs in headless mode by default, but Wine (and winecfg) don&#8217;t. Which caused some interesting issues. Fortunately, there&#8217;s <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/nix/wine-win11-shell.nix#L17">xvfb-run</a> which will stub out the GUI stuff so I can effectively run GUI apps in headless mode. But after all that, it worked!</p><p>Except, it was slow.</p><p>The hardware I&#8217;m using for my server isn&#8217;t <em>bad</em>, but it is <em>older</em> and energy <em>efficient</em> - so it&#8217;s not all that quick. Plus, now I was running inside a container, and the container was acting - funny. I hadn&#8217;t quite nailed it down yet (I would soon). Plus, everything was running <em>in-serial</em>, when it didn&#8217;t really have to.</p><h2>Part 3: Optimizing</h2><p>Fortunately, I&#8217;ve had to fix a lot of build systems at work. So I had a really good idea on what was going on, and how to fix it.</p><p>The first issue was everything was serial - which was needed for the dependencies between generating files. Well, that&#8217;s okay. First step is to simply break it from one command to a series of jobs to run each nix-shell, and then I can reorder, parallelize, or remove really slow bits that I don&#8217;t care about. But once I get it into jobs, it&#8217;ll be easier to work with.</p><p>So I did, and then very quickly realized what that funky behavior was I was seeing. Every job that couldn&#8217;t generate code was now failing. And this was because every job was getting a <em>new</em> docker container. It was never reusing a container - even between jobs. This was different than what I&#8217;m used to where a clean step was mandatory since the same git repo would be reused between runs.</p><p>Okay, so quick patch is to just run a job that can generate files in every step. No sweat.</p><p>Once that was done, I started parallelizing (which just meant spinning up new runners).</p><p>But, the fact I was regenerating files everywhere bugged me. I only really needed to generate once and download everywhere it was needed. So, I took a look at Forgejo artifacts, realized it was easy to upload and then download output between steps, and then proceeded to add <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/.forgejo/workflows/change.yml#L9-L14">uploading of generated sources</a> followed by <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/.forgejo/workflows/change.yml#L129-L131">downloading of those sources</a> everywhere it was needed. I then had a parallel build system with only a few sections in serial.</p><p>I then noticed something interesting: Forgejo runs in the order declared whenever it can. So I did one more trick, I put the blocking steps <em>first</em>. That way the blocked steps would be unblocked as quickly as possible.</p><p>And with that, I had my build system!</p><h3>Where I&#8217;m headed</h3><p>So far, I only have Linux and Windows support. And right now, Windows builds are only automated through MinGW - I still have to do MSVC builds manually. What I want to do is automated MSVC builds somehow (maybe through a VM or wine or native windows box, not sure yet).</p><p>Additionally, I want to add support for OSX. There are a few places that will need to be updated, but it shouldn&#8217;t be too hard. I have a Mac laying around somewhere - I just need to get it dusted off and add in the platform-specific wrappers where they&#8217;re needed.</p><p>After that, we&#8217;ll I&#8217;ll probably keep working on the library again. I have a lot of functionality I&#8217;m trying to add as I build my way up to an application. Right now I&#8217;m focused on adding in more testing functionality. I&#8217;ll then want to add a network stack and a window management system - which will bring oh so many levels of &#8220;fun&#8221; when trying to merge it with the current build pipeline.</p><p>At some point I&#8217;ll start adding support for more platforms. Arduino is pretty high on my list (I already have Raspberry Pi), as well as probably another micro-controller or single-board computer or two. Android and iOS support would be nice at some point, but I&#8217;m not really big into mobile development so it&#8217;s not a high priority for me yet. FreeBSD support would also be interesting - but again it&#8217;s not very high on my list. I&#8217;ve only used FreeBSD a handful of times, but that&#8217;s always been exclusively in a VM. I know that some work is needed since currently I have a few Linux-specific APIs instead of only generic POSIX APIs (yes, there&#8217;s a difference).</p><p>At some point, I&#8217;ll get more of my other projects moved over. So far I&#8217;m liking the nix-shell system, especially now that I&#8217;ve gotten a lot of the tool-chains working.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Technically, I also have a <a href="https://git.matthewtolman.dev/mtolman/peaksc/src/commit/ddd42b1a82dd2b27364d1c1a0a99c664360f740d/CMakeLists.txt">CMake setup</a>. However, I mostly use that for IDE support (like CLion and Visual Studio). I do try to keep the build.sh and CMake systems in-sync. However, I chose not to build my nix-shell stuff around CMake since that&#8217;s just a lot more complexity, and honestly bash scripting this stuff isn&#8217;t that hard.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Nix: The wounded siren]]></title><description><![CDATA[A few months ago I switched from Fedora to NixOS as my daily driver for my laptop.]]></description><link>https://matthewtolman.com/p/nix-the-wounded-siren</link><guid isPermaLink="false">https://matthewtolman.com/p/nix-the-wounded-siren</guid><dc:creator><![CDATA[Matt Tolman]]></dc:creator><pubDate>Sun, 14 Dec 2025 04:47:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/597428b1-50b9-4476-a7e7-faed5987f708_1200x830.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few months ago I switched from Fedora to NixOS as my daily driver for my laptop. So far I&#8217;ve liked it enough I recently replaced Ubuntu on my home server with NixOS as well. It called to me like a siren, and I have been captivated by that call.</p><p>Yet, the call is not perfect. It&#8217;s remarkably imperfect. So much so that unlike many &#8220;hype&#8221; technologies, this one struggles to be recommended by those who it ensnares - me included.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matthewtolman.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Coding with Matt! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>This realization didn&#8217;t dawn on me until after a few different conversations I had recently. For the first, I was talking to someone who was switching from Windows to Linux. They asked what I used, and I replied that I use NixOS - but that I don&#8217;t recommend it. I instead recommended Fedora or Mint or one of the other distros. </p><p>Later, I talked to someone who was an experienced Linux user about which distros we were both using. Again, I said I use NixOS - but that I don&#8217;t recommend it.</p><p>Which posses the question - if I love NixOS so much I&#8217;m switching my daily driver and my servers to it, why am I not recommending it?</p><h2>The Software &#8220;Problem&#8221;</h2><p>Nix is built around a solution to a problem that you may - or may not - have. The entire philosophy around how everything works is built around this solution. And that brings a lot of headaches. And that problem is how software is installed and managed on your machine.</p><p>This problem ranges from simply installing different versions of the same software, to installing software with many shared - and sometimes conflicting - libraries. There are lots of solutions to this problem - package managers, homebrew, installers, flatpaks, etc. However, Nix takes the problem deeper than simply &#8220;installing&#8221; software or &#8220;isolating&#8221; software.</p><p>Nix strives to make software installation reproducible, declarative, and reliable.</p><p>In other words, you should be able to say &#8220;I want this software with these settings&#8221; and it should just happen. You can even say &#8220;for this project I want this <em>version</em> of this software with these settings, and for this other project I want a <em>different</em> version and different settings&#8221; and Nix will do it.</p><p>This is a very specific problem, and one many people have not had. Often because people usually don&#8217;t care about the version or settings - so long as the software works. </p><p>However, I <em>do</em> care about the version and settings.</p><h3>Legacy Code, Interpreters, and Compilers</h3><p>One of the big things I&#8217;ve done through my career - at least so far - is work on legacy systems. These are systems with not just old code, but old versions of stuff. Things like outdated build systems, language versions, interpreters, run times, native extensions, etc.</p><p>Often, the new version has some series of major breaking changes - or it simply doesn&#8217;t exist. The language extension the whole system was built around just simply stopped being maintained 6 years ago, and now the new language runtime made some internal change so now the extension won&#8217;t work.</p><p>When this happens, a rewrite or refactor or new system is usually started with the latest software - but it too quickly becomes legacy. Rinse and repeat a few times, and soon there&#8217;s a mess of legacy systems with very different version dependencies and incompatibilities all somehow working together to get a job done.</p><p>And this is the type of situation that I don&#8217;t just work in, but I thrive in.</p><p>These old systems are a canvas of opportunities. They also tend to be what makes the business the most money - way more than that shiny new microservice someone just built. That means these services are important to care for, extend, maintain, grow, and develop.</p><p>But, we very quickly run into a problem. Getting just one of these systems running in on a development machine is difficult. Getting two is excruciating. Three is almost impossible. Very quickly package systems start fighting.</p><p>I&#8217;ve had homebrew completely break my PHP runtime because it updated my NodeJS install which then bumped some shared library and deleted the old version. I&#8217;ve had NVM install x86 libraries on an M2 mac which then prevented Homebrew from installing an Arm version of Ruby since it tried to link to those x86 libraries. I&#8217;ve had Node 18 be unable to build a Node 14 project, and Node 20 be unable to run <code>npm install</code> for a Node 18 project. I&#8217;ve seen apt-get delete old versions of a compiler, homebrew installs get unlinked by the OS, nvm lose it&#8217;s mind, asdf get removed from the path by homebrew, snap just completely stop working, flatpak breaking auto updates, dotnet CLI completely brick itself trying to update, and so much more. Not to mention managing database versions, port conflicts, VPN, XCode developer tools, MSVC compiler toolchain, Windows vs Linux vs OSX incompatibilities, etc. </p><p>And then there&#8217;s the whole &#8220;breaking changes&#8221; in programming languages. I&#8217;ve gone through the JVM 8 to 9 to 11 trek, the PHP 5 to 7 to 8 landslide, and the Python 2 to 3 transition. And then there&#8217;s TypeScript which broke so many things when it introduced the &#8220;unknown&#8221; type.</p><p>And things just continue for my side projects. I love learning what&#8217;s new in languages. But when some languages have multiple implementations (e.g. C++), this quickly becomes an issue. Especially when I ever try sharing my code with someone and they have a different compiler <em>version</em> which has different <em>syntax rules</em>.</p><p>All this to say, I very much <em>do</em> care about what versions and settings I have. Having an easy way to manage a version is the difference between me having a productive day or spending a week rebuilding my developer machine.</p><p>And I&#8217;ve tried a lot of options. Vagrant, Docker, cloud workspaces, separate machines for different configurations, etc. None of them seemed to stick.</p><h2>The Nix &#8220;Solution&#8221;</h2><p>And then came along Nix. Promising to fix all of my problems.</p><p>Well, it did do a really good job addressing that one problem - and then gave me quite a few (more on that in a bit). But it really did a good job addressing my main problem - installing things intelligently and in an isolated way without breaking other things.</p><p>How it does it is similar to Docker or Vagrant or cloud workspaces at first. There&#8217;s a file with some custom DSL that describes how to build a machine, and then some program uses that to build the machine. The difference though, is that this isn&#8217;t running in some container or virtualization layer (which always brings headaches when trying to interface with said layer). Instead, it&#8217;s running natively on your machine. So much so that in my case, it <em>is</em> my machine.</p><p>What&#8217;s even better is it can archive a configuration so that you can rollback to a previous version. This is awesome when I&#8217;m trying to &#8220;improve&#8221; my Linux distribution. If I totally screw something up (like install a new login window and forget to disable the old one) then I can revert to the previous install that wasn&#8217;t broken.</p><p>This ability to &#8220;revert&#8221; goes into user-space as well. We can create a temporary &#8220;machine&#8221; with a nix shell, and that machine can now operate with different versions of compilers, new programs, or even remove programs we have installed &#8220;globally.&#8221; Then when we&#8217;re done, we can exit the shell thereby &#8220;reverting&#8221; to our machine&#8217;s global configuration.</p><p>And, since Nix is configuration driven, we can write config files describing those temporary machines and add them to our source control. This allows us to then share those temporary machines with other nix users so they can then run those machines locally. And again, with NixOS it&#8217;s not a VM - it&#8217;s their actual machine. With full access to their file system, with full access to the GUI, hardware, etc. No SSH, no remote debugging, no slowness from a VM hogging all the memory/CPU. It&#8217;s just a temporary &#8220;update&#8221; for their machine.</p><p>And, all of these shells are <em>isolated</em>. So you could have two, or three, or four &#8220;versions&#8221; of your machine running simultaneously and reading from the same data on your system.</p><p>We can then take it a step further and build VMs with nix for things we couldn&#8217;t do natively on our machine - like testing x86, ARM, RISC-V, PowerPC, and MIPS versions of our code all at once - <a href="https://gitlab.com/mtolman/peaksc/-/tree/main/nix?ref_type=heads">something I recently did for one of my projects</a>.</p><p>This is where I fell in love with Nix. It gave me something that solves the problem I have, and which wastes so much of my time.</p><p>Of course, no lunch comes for free, and Nix has a lot of drawbacks.</p><h2>Why I don&#8217;t recommend Nix</h2><p>Again, most people don&#8217;t usually have the problem I have. They just want to install a default option to have things work by default. They don&#8217;t care about comparing options, or having the ability to switch versions, or anything like that. They&#8217;re usually not installing a lot of things anyway. So, does Nix serve these types of people?</p><p>No, not really.</p><p>One of Nix&#8217;s biggest drawbacks is that <em>everything is driven by a global config file</em>. Your installed programs, user list, firewall, hosts file, timezone, printer discovery settings, etc. are all determined by <code>/etc/nix/configuration.nix</code>. Changing this file requires root access. Once the file is changed, it needs to be reloaded with a special command (also requiring root access). And, this file is in a unique &#8220;nix&#8221; configuration language. It&#8217;s not YAML, or JSON, or Lua, or TOML, or C, or JavaScript, or Bash, anything but Nix. This makes it a steep learning curve to just install a new program.</p><p>Not only that, but not everything works out of the box. It turns out, every time I installed NixOS, Bluetooth was <em>disabled by default!</em> And when I installed it on my laptop, I didn&#8217;t have proper WiFi drivers since I had to download and install a special hardware configuration for my laptop first (luckily I had an Ethernet adapter which worked, but this was terrifying for a bit!).</p><p>And then there&#8217;s a &#8220;fun&#8221; little quirk where you can&#8217;t just run executables built for generic Linux. I tried doing that, and I got a little pop-up saying that NixOs can&#8217;t open executables made for generic linux. So, things need to be specially packaged. This makes getting printer drivers installed a real pain (way more painful than other Linux distros).</p><p>Plus, there&#8217;s just some general annoyances that get in the way. Nix isn&#8217;t necessarily an immutable distro, but it does make a lot of standard configuration files immutable (like your bash/zsh configuration file, hosts file, etc).</p><p>Also, nix documentation is horrendously fragmented, and the parts you do find often comes across as some random person&#8217;s notes they wrote on the back of a napkin before wiping ketchup all over it and throwing it in the trash. Like, there&#8217;s definitely signs of a &#8220;eureka&#8221; moment someone had, but there&#8217;s no context at all as to where it should go in the configuration file, or what dependencies it needs, or really what anything means, and that page is really old and most likely will never be updated.</p><p>And then there&#8217;s the little things that are just broken in my day-to-day life, but I&#8217;ve become so used to them and developed so many workarounds by now that I&#8217;d only ever know about them if someone sat down watching me use my computer and then immediately point out everything that doesn&#8217;t work well. I know that stuff is there, but I love the fact that the many versions of each random compiler or interpreter I use works way more reliably that I just don&#8217;t notice the stuff that&#8217;s broken anymore.</p><p>These pain points, the wounds of nix, limit the reach of it&#8217;s call. The tune is filled with both promise and despair. A beautiful melody is maimed by an ugly harmony. At some point, perhaps the siren will no longer be encumbered by it&#8217;s short comings. And then, at that day, perchance those who it&#8217;s enraptured will be able to joyfully sing it&#8217;s praise. But until then, it&#8217;s tune only charms those who can focus on only the benefits and become blind to the defects.</p><p></p><p>P.S. For those who do want to try out nix, I don&#8217;t recommend starting with nix as a package manager/shell on another distro (e.g. debian). I know it&#8217;s an option, but it really sucks since at that point it&#8217;s just another opinionated package manager conflicting with everything else you&#8217;re doing. It was an awful experience that made me almost give up on nix as just another gimmicky package manager. A lot of the shortcomings of the standalone nix shell are addressed in NixOS.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matthewtolman.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Coding with Matt! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>