Design Token-Based UI Architecture

Mike's Notes

Pipi 9 has an existing design system engine, one of its many parts. This engine describes the CSS files but does not yet automate or generate code.

Design Tokens have significantly matured, and the draft standard has recently improved.

The article below is copied from Martin Fowler's website. It describes how ThoughtWorks uses Design Tokens for code generation, which I will use as a starting point. It should work well with the existing design system engine.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library > Authors > Martin Fowler
  • Home > Handbook > 

Last Updated

17/05/2025

Design Token-Based UI Architecture

By: Andreas Kutschmann
MartinFowler.com: December 12 2024

Design tokens are design decisions as data and serve as a single source of truth for design and engineering. Utilizing deployment pipelines, they enable automated code generation across platforms, allowing for faster updates and improved consistency in design. Organizing tokens in layers—progressing from available options to tokens that capture how they are applied—ensures scalability and a better developer experience. Keeping option tokens (e.g. color palettes) private reduces file size and supports non-breaking changes. These benefits make design tokens particularly well-suited for organizations with large-scale projects, multi-platform environments or frequent design changes.

Contents

  • Role of design tokens
    • What are design tokens?
  • Establishing a single source of truth
  • Automated design token distribution
    • Fully automated pipeline
    • Pipeline including manual approval
  • Organizing tokens in layers
    • Option tokens: defining what design options are provided
    • Decision tokens: defining how styles are applied
    • Component tokens: defining where styles are applied
    • How many layers shall I use?
  • Token scope
    • File-based scope
    • A more flexible approach
  • Should I use design tokens?
    • When to use design tokens
    • When design tokens might not be necessary

Design tokens, or “tokens” are fundamental design decisions represented as data. They are the foundational building blocks of design systems.

Since the release of the second editor’s draft of the design token specification in 2022 and the call for tool makers to start implementing and providing feedback, the landscape of design token tools has evolved rapidly. Tools like code generators, documentation systems, and UI design software are now better equipped to support design tokens, underscoring their growing importance in modern UI architecture.

In this article, I'll explain what design tokens are, when they are useful and how to apply them effectively. We'll focus on key architectural decisions that are often difficult to change later, including:

  1. How to organize design tokens in layers to balance scalability, maintainability and developer experience.
  2. Whether all tokens should be made available to product teams or just a subset.
  3. How to automate the distribution process of tokens across teams.

Role of design tokens

Around 2017, I was involved in a large project that used the Micro Frontend Architecture to scale development teams. In this setup, different teams were responsible for different parts of the user interface, which could be even on the same page. Each team could deploy its micro-frontend independently.

There were various cases where components would be displayed on top of each other (such as dialogs or toasts appearing on top of content areas), which were not part of the same micro frontend. Teams used the CSS property z-index to control the stacking order, often relying on magic numbers—arbitrary values that weren’t documented or standardized. This approach did not scale as the project grew. It led to issues that took effort to fix, as cross-team collaboration was needed.

The issue was eventually addressed with design tokens and I think makes a good example to introduce the concept. The respective token file might have looked similar to this:

{
  "z-index": {
    "$type": "number",
    "default": {
      "$value": 1
    },
    "sticky": {
      "$value": 100
    },
    "navigation": {
      "$value": 200
    },
    "spinner": {
      "$value": 300
    },
    "toast": {
      "$value": 400
    },
    "modal": {
      "$value": 500
    }
  }
}

The design tokens above represent the set of z-index values that can be used in the application and the name gives developers a good idea of where to use them. A token file like this can be integrated into the designers’ workflow and also be used to generate code, in a format that each team requires. For example, in this case, the token file might have been used to generate CSS or SCSS variables:

css

:root {
    --z-index-default: 1;
    --z-index-sticky: 100;
    --z-index-navigation: 200;
    --z-index-spinner: 300;
    --z-index-toast: 400;
    --z-index-modal: 500;
  }

scss

$z-index-default: 1;
  $z-index-sticky: 100;
  $z-index-navigation: 200;
  $z-index-spinner: 300;
  $z-index-toast: 400;
  $z-index-modal: 500;

What are design tokens?

Salesforce originally introduced design tokens to streamline design updates to multiple platforms.

The Design Tokens Community Group describes design tokens as “a methodology for expressing design decisions in a platform-agnostic way so that they can be shared across different disciplines, tools, and technologies

Let’s break this down:

Cross-Disciplinary Collaboration: Design tokens act as a common language that aligns designers, developers, product managers, and other disciplines. By offering a single source of truth for design decisions, they ensure that everyone involved in the product life cycle is on the same page, leading to more efficient workflows.

Tool integration: Design tokens can be integrated into various design and development tools, including UI design software, token editors, translation tools (code generators), and documentation systems. This enables design updates to be quickly reflected in the code base and are synchronized across teams.

Technology adaptability: Design tokens can be translated into different technologies like CSS, SASS, and JavaScript for the web, and even used on native platforms like Android and iOS. This flexibility enables design consistency across a variety of platforms and devices.

Establishing a single source of truth

A key benefit of design tokens is their ability to serve as a single source of truth for both design and engineering teams. This ensures that multiple products or services maintain visual and functional consistency.

A translation tool takes one or more design token files as input and generates platform-specific code as output. Some translation tools can also produce documentation for the design tokens in the form of HTML. At the time of writing, popular translation tools include Style Dictionary, Theo, Diez or Specify App.


Figure 1: Translation tool

Automated design token distribution

In this section, we’ll explore how to automate the distribution of design tokens to product teams.

Let’s assume our goal is to provide teams with updated, tech-specific design tokens immediately after a designer makes a change. To achieve this, we can automate the translation and distribution process using a deployment pipeline for design tokens. Besides platform-specific code artifacts (like CSS for the web, XML for Android etc.), the pipeline might also deploy the documentation for the design tokens.

One crucial requirement is keeping design tokens under version control. Thankfully, plugins for popular design tools like Figma already integrate with Git providers like GitHub. It's considered best practice to use the Git repository as the single source of truth for design tokens—not the design tool itself. However, this requires the plugin to support syncing both ways between the repository and the design tool, which not all plugins do. As of now, Tokens Studio is a plugin that offers this bidirectional syncing. For detailed guidance on integrating Tokens Studio with different Git providers, please refer to their documentation. The tool enables you to configure a target branch and supports a trunk-based as well as a pull-request-based workflow.

Once the tokens are under version control, we can set up a deployment pipeline to build and deploy the artifacts needed by the product teams, which include platform-specific source code and documentation. The source code is typically packaged as a library and distributed via an artifact registry. This approach gives product teams control over the upgrade cycle. They can adopt updated styles by simply updating their dependencies. These updates may also be applied indirectly through updates of component libraries that use the token-based styles.


Figure 2: Automated design token distribution

This overall setup has allowed teams at Thoughtworks to roll out smaller design changes across multiple front-ends and teams in a single day.

Fully automated pipeline

The most straightforward way to design the pipeline would be a fully automated trunk-based workflow. In this setup, all changes pushed to the main branch will be immediately deployed as long as they pass the automated quality gates.

Such a pipeline might consist of the following jobs:

Check: Validate the design token files using a design token validator or a JSON validator.

  • Build: Use a translation tool like Style Dictionary to convert design token files into platform-specific formats. This job might also build the docs using the translation tool or by integrating a dedicated documentation tool.
  • Test: This job is highly dependent on the testing strategy. Although some tests can be done using the design token file directly (like checking the color contrast), a common approach is to test the generated code using a documentation tool such as Storybook. Storybook has excellent test support for visual regression tests, accessibility tests, interaction tests, and other test types.
  • Publish: Publish updated tokens to a package manager (for example, npm). The release process and versioning can be fully automated with a package publishing tool that is based on Conventional Commits like semantic-release. semantic-release also allows the deployment of packages to multiple platforms. The publish job might also deploy documentation for the design tokens.
  • Notify: Inform teams of the new token version via email or chat, so that they can update their dependencies.

    Figure 3: Fully automated deployment pipeline

    Pipeline including manual approval

    Sometimes fully automated quality gates are not sufficient. If a manual review is required before publishing, a common approach is to deploy an updated version of the documentation with the latest design token to a preview environment (a temporary environment).

    If a tool like Storybook is used, this preview might contain not only the design tokens but also show them integrated with the components used in the application.

    An approval process can be implemented via a pull-request workflow. Or, it can be a manual approval / deployment step in the pipeline.


    Figure 4: Deployment pipeline with manual approval

    Organizing tokens in layers

    As discussed earlier, design tokens represent design decisions as data. However, not all decisions operate at the same level of detail. Instead, ideally, general design decisions guide more specific ones. Organizing tokens (or design decisions) into layers allows designers to make decisions at the right level of abstraction, supporting consistency and scalability.

    For instance, making individual color choices for every new component isn’t practical. Instead, it’s more efficient to define a foundational color palette and then decide how and where those colors are applied. This approach reduces the number of decisions while maintaining a consistent look and feel.

    There are three key types of design decisions for which design tokens are used. They build on top of one another:

    • What design options are available to use?
    • How are those styles applied across the user interface?
    • Where exactly are those styles applied (in which components)?

    There are various names for these three types of tokens (as usual, naming is the hard part). In this article, we’ll use the terms proposed by Samantha Gordashko: option tokens, decision tokens and component tokens.

    Let’s use our color example to illustrate how design tokens can answer the three questions above.

    Option tokens: defining what design options are provided

    Option tokens (also called primitive tokens, base tokens, core tokens, foundation tokens or reference tokens) define what styles can be used in the application. They define things like color palettes, spacing/sizing scales or font families. Not all of them are necessarily used in the application, but they present reasonable options.

    Using our example, let’s assume we have a color palette with 9 shades for each color, ranging from very light to highly saturated. Below, we define the blue tones and grey tones as option-tokens:

    {
      "color": {
        "$type": "color",
        "options": {
          "blue-100": {"$value": "#e0f2ff"},
          "blue-200": {"$value": "#cae8ff"},
          "blue-300": {"$value": "#b5deff"},
          "blue-400": {"$value": "#96cefd"},
          "blue-500": {"$value": "#78bbfa"},
          "blue-600": {"$value": "#59a7f6"},
          "blue-700": {"$value": "#3892f3"},
          "blue-800": {"$value": "#147af3"},
          "blue-900": {"$value": "#0265dc"},
          "grey-100": {"$value": "#f8f8f8"},
          "grey-200": {"$value": "#e6e6e6"},
          "grey-300": {"$value": "#d5d5d5"},
          "grey-400": {"$value": "#b1b1b1"},
          "grey-500": {"$value": "#909090"},
          "grey-600": {"$value": "#6d6d6d"},
          "grey-700": {"$value": "#464646"},
          "grey-800": {"$value": "#222222"},
          "grey-900": {"$value": "#000000"},
          "white": {"$value": "#ffffff"}
        }
      }
    }

    Although it’s highly useful to have reasonable options, option tokens fall short of being sufficient for guiding developers on how and where to apply them.

    Decision tokens: defining how styles are applied

    Decision tokens (also called semantic tokens or system tokens) specify how those style options should be applied contextually across the UI.

    In the context of our color example, they might include decisions like the following:

    • grey-100 is used as a surface color.
    • grey-200 is used for the background of disabled elements.
    • grey-400 is used for the text of disabled elements.
    • grey-900 is used as a default color for text.
    • blue-900 is used as an accent color.
    • white is used for text on accent color backgrounds.

    The corresponding decision token file would look like this:

    {
      "color": {
        "$type": "color",
        "decisions": {
          "surface": {
            "$value": "{color.options.grey-100}",
            "description": "Used as a surface color."
          },
          "background-disabled": {
            "$value": "{color.options.grey-200}",
            "description":"Used for the background of disabled elements."
          },
          "text-disabled": {
            "$value": "{color.options.grey-400}",
            "description": "Used for the text of disabled elements."
          },
          "text": {
            "$value": "{color.options.grey-900}",
            "description": "Used as default text color."
          },
          "accent": {
            "$value": "{color.options.blue-900}",
            "description": "Used as an accent color."
          },
          "text-on-accent": {
            "$value": "{color.options.white}",
            "description": "Used for text on accent color backgrounds."
          }
        }
      }
    }

    As a developer, I would mostly be interested in the decisions, not the options. For example, color tokens typically contain a long list of options (a color palette), while very few of those options are actually used in the application. The tokens that are actually relevant when deciding which styles to apply, would be usually the decision tokens.

    Decision tokens use references to the option tokens. I think of organizing tokens this way as a layered architecture. In other articles, I have often seen the term tier being used, but I think layer is the better word, as there is no physical separation implied. The diagram below visualizes the two layers we talked about so far:


    Figure 5: 2-layer pattern

    Component tokens: defining where styles are applied

    Component tokens (or component-specific tokens) map the decision tokens to specific parts of the UI. They show where styles are applied.

    The term component in the context of design tokens does not always map to the technical term component. For example, a button might be implemented as a UI component in some applications, while other applications just use the button HTML element instead. Component tokens could be used in both cases.

    Component tokens can be organised in a group referencing multiple decision tokens. In our example, this references might include text- and background-colors for different variants of the button (primary, secondary) as well as disabled buttons. They might also include references to tokens of other types (spacing/sizing, borders etc.) which I'll omit in the following example:

    {
      "button": {
        "primary": {
          "background": {
            "$value": "{color.decisions.accent}"
          },
          "text": {
            "$value": "{color.decisions.text-on-accent}"
          }
        },
        "secondary": {
          "background": {
            "$value": "{color.decisions.surface}"
          },
          "text": {
            "$value": "{color.decisions.text}"
          }
        },
        "background-disabled": {
          "$value": "{color.decisions.background-disabled}"
        },
        "text-disabled": {
          "$value": "{color.decisions.text-disabled}"
        }
      }
    }

    To some degree, component tokens are simply the result of applying decisions to specific components. However, as this example shows, this process isn’t always straightforward—especially for developers without design experience. While decision tokens can offer a general sense of which styles to use in a given context, component tokens provide additional clarity.


    Figure 6: 3-layer pattern

    Note: there may be “snowflake” situations where layers are skipped. For example, it might not be possible to define a general decision for every single component token, or those decisions might not have been made yet (for example at the beginning of a project).

    How many layers shall I use?

    Two or three layers are quite common amongst the bigger design systems.

    However, even a single layer of design tokens already greatly limits the day-to-day decisions that need to be made. For example, just deciding what units to use for spacing and sizing became a somewhat nontrivial task with up to 43 units for length implemented in some browsers (if I counted correctly).

    A three-layer architecture should offer the best developer experience. However, it also increases maintenance effort and token count, as new tokens are introduced with each new component. This can result in a larger code base and heavier package size.

    Starting with two layers (option and decision tokens) can be a good idea for projects where the major design decisions are already in place and/or relatively stable. A third layer can still be added if there is a clear need.

    An additional component layer makes it easier for designers to change decisions later or let them evolve over time. This flexibility could be a driving force for a three-layer architecture. In some cases, it might even make sense to start with component tokens and to add the other layers later on.

    Ultimately, the number of layers depends on your project's needs and how much flexibility and scalability are required.

    Token scope

    I already mentioned that while option tokens are very helpful to designers, they might not be relevant for application developers using the platform-specific code artifacts. Application developers will typically be more interested in the decision/component tokens.

    Although token scope is not yet included in the design token spec, some design systems already separate tokens into private (also called internal) and public (also called global) tokens. For example, the Salesforce Lightning Design System introduced a flag for each token. There are various reasons why this can be a good idea:

    • it guides developers on which tokens to use
    • fewer options provide a better developer experience
    • it reduces the file size as not all tokens need to be included
    • private/internal tokens can be changed or removed without breaking changes

    A downside of making option tokens private is that developers would rely on designers to always make those styles available as decision or component tokens. This could become an issue in case of limited availability of the designers or if not all decisions are available, for example at the start of a project.

    Unfortunately, there is no standardized solution yet for implementing scope for design tokens. So the approach depends on the tool-chain of the project and will most likely need some custom code.

    File-based scope

    Using Style Dictionary, we can use a filter to expose only public tokens. The most straightforward approach would be to filter on the file ending. If we use different file endings for component, decision and option tokens, we can use a filter on the file path, for example, to make the option tokens layer private.

    Style Dictionary config

    const styleDictionary = new StyleDictionary({
        "source": ["color.options.json", "color.decisions.json"],
        "platforms": {
          "css": {
            "transformGroup": "css",
            "files": [
              {
                "destination": "variables.css",
                "filter": token => !token.filePath.endsWith('options.json'),
                "format": "css/variables"
              }
            ]
          }
        }
      });

    The resulting CSS variables would contain only these decision tokens, and not the option tokens.

    Generated CSS variables

    :root {
        --color-decisions-surface: #f8f8f8;
        --color-decisions-background-disabled: #e6e6e6;
        --color-decisions-text-disabled: #b1b1b1;
        --color-decisions-text: #000000;
        --color-decisions-accent: #0265dc;
        --color-decisions-text-on-accent: #ffffff;
      }

    A more flexible approach

    If more flexibility is needed, it might be preferable to add a scope flag to each token and to filter based on this flag:

    Style Dictionary config

     const styleDictionary = new StyleDictionary({
        "source": ["color.options.json", "color.decisions.json"],
        "platforms": {
          "css": {
            "transformGroup": "css",
            "files": [
              {
                "destination": "variables.css",
                "filter": {
                  "public": true
                },
                "format": "css/variables"
              }
            ]
          }
        }
      });

    If we then add the flag to the decision tokens, the resulting CSS would be the same as above:

    Tokens with scope flag

     {
        "color": {
          "$type": "color",
          "decisions": {
            "surface": {
              "$value": "{color.options.grey-100}",
              "description": "Used as a surface color.",
              "public": true
            },
            "background-disabled": {
              "$value": "{color.options.grey-200}",
              "description":"Used for the background of disabled elements.",
              "public": true
            },
            "text-disabled": {
              "$value": "{color.options.grey-400}",
              "description": "Used for the text of disabled elements.",
              "public": true
            },
            "text": {
              "$value": "{color.options.grey-900}",
              "description": "Used as default text color.",
              "public": true
            },
            "accent": {
              "$value": "{color.options.blue-900}",
              "description": "Used as an accent color.",
              "public": true
            },
            "text-on-accent": {
              "$value": "{color.options.white}",
              "description": "Used for text on accent color backgrounds.",
              "public": true
            }
          }
        }
      }

    Generated CSS variables

    :root {
        --color-decisions-surface: #f8f8f8;
        --color-decisions-background-disabled: #e6e6e6;
        --color-decisions-text-disabled: #b1b1b1;
        --color-decisions-text: #000000;
        --color-decisions-accent: #0265dc;
        --color-decisions-text-on-accent: #ffffff;
      }

    Such flags can now also be set through the Figma UI (if using Figma variables as a source of truth for design tokens). It is available as hiddenFromPublishing flag via the Plugins API.

    Should I use design tokens?

    Design tokens offer significant benefits for modern UI architecture, but they may not be the right fit for every project.

    Benefits include:

    • Improved lead time for design changes
    • Consistent design language and UI architecture across platforms and technologies
    • Design tokens being relatively lightweight from an implementation point of view

    Drawbacks include:

    • Initial effort for automation
    • Designers might have to (to some degree) interact with Git
    • Standardization is still in progress

    Consider the following when deciding whether to adopt design tokens:

    When to use design tokens

    1. Multi-Platform or Multi-Application Environments: When working across multiple platforms (web, iOS, Android…) or maintaining several applications or frontends, design tokens ensure a consistent design language across all of them.
    2. Frequent Design Changes: For environments with regular design updates, design tokens provide a structured way to manage and propagate changes efficiently.
    3. Large Teams: For teams with many designers and developers, design tokens facilitate collaboration.
    4. Automated Workflows: If you’re familiar with CI/CD pipelines, the effort to add a design token pipeline is relatively low. There are also commercial offerings.

    When design tokens might not be necessary

    1. Small projects: For smaller projects with limited scope and minimal design complexity, the overhead of managing design tokens might not be worth the effort.
    2. No issue with design changes: If the speed of design changes, consistency and collaboration between design and engineering are not an issue, then you might also not need design tokens.

    Acknowledgments

    Thanks to Berni Ruoff—I don't think I would have written this article without all the great discussions we had about design systems and design tokens (and for giving feedback on the first draft). Thanks to Shawn Lukas, Jeen Suratriyanont, Mansab Uppal and of course Martin for all the feedback on the subsequent drafts.

    Growing the development forest - with Martin Fowler

    Mike's Notes

    This interview with Martin Fowler was in a recent Refactoring Newsletter.

    Resources

    References

    • Reference

    Repository

    • Home > Ajabbi Research > Library > Subscriptions > Refactoring
    • Home > Handbook > 

    Last Updated

    17/05/2025

    Growing the development forest - with Martin Fowler

    By: Luca Rossi
    Refactoring: 24/01/2024

    Martin is chief scientist at ThoughtWorks. He is one of the original signatories of the Agile Manifesto and author of several legendary books, among which there is Refactoring, which shares the name with this podcast and this newsletter. 

    With Martin, we talked about the impact of AI on software development, from the development process to how human learning and understanding changes up to the future of software engineering jobs.

    Then we explored the technical debt metaphor, why it has been so successful, and Martin's own advice on dealing with it. And finally, we talked about the state of Agile, the resistance that still exists today towards many Agile practices and how to measure engineering effectiveness.

    (03:29) Introduction
    (05:20) Development cycle with AI
    (08:36) Less control and reduced learning
    (13:11) Splitting task between Human and AI
    (14:48) The skills shift
    (20:17) Betting on new technologies
    (27:22) Martin's Refactoring and technical debt
    (29:24) Accumulating "cruft"
    (33:14) Dealing with "cruft"
    (37:24) The financial value of refactoring
    (42:04) Measuring performances
    (46:19) Why the "forest" didn't spread
    (56:11) Make the forest appealing

    Show notes / useful links:

    Feature Flags Transform Your Product Development Workflow

    Mike's Notes

    Ben Nadel wrote a great book on Feature Flags. He has now made the online version free to read.

    Ben is also very generous in sharing his CFML code, clearly explaining how it works, and answering questions. I learn a lot from Ben.

    There is also a playground demo to play with feature flags.

    He also has a lot of helpful YouTube videos in which he explains a lot of the code.

    Resources

    References

    • Reference

    Repository

    • Home > Ajabbi Research > Library > Authors > Ben Nadel
    • Home > Handbook > 

    Last Updated

    17/05/2025

    Feature Flags Playground Demo

    By: Ben Nadel
    bennadel.com:


    The four kinds of optimisation

    Mike's Notes

    This is an excerpt from an article referenced in a recent issue of Quastor.

    It's a thoughtful article about ways to improve software performance.

    Laurence Tratt is a programmer and the Shopify / Royal Academy of Engineering Research Chair in Language Engineering in the Department of Informatics at King’s College London, where he leads the Software Development Team.

    Resources

    References

    • Reference

    Repository

    • Home > Ajabbi Research > Library > Subscriptions > Quastor
    • Home > Handbook > 

    Last Updated

    17/05/2025

    Four Kinds of Optimisation

    By: Laurence Tratt
    tratt.net: 14/11/2023

    Premature optimisation might be the root of all evil, but overdue optimisation is the root of all frustration. No matter how fast hardware becomes, we find it easy to write programs which run too slow. Often this is not immediately apparent. Users can go for years without considering a program’s performance to be an issue before it suddenly becomes so — often in the space of a single working day.

    I have devoted more of my life to optimisation than I care to think about, and that experience has led me to make two observations:

    • Human optimism leads us to believe that we can easily know where a program spends most of its time.
    • Human optimism leads us to believe that we can easily know how to make the slow parts of a program run faster.

    You will not be surprised to learn that I think both forms of optimism misplaced. Partly this is because, as hardware and software have become more sophisticated, it has become harder to understand their effects on performance. But, perhaps more fundamentally, we tend to overestimate how much we know about the software we’re working on. We overemphasise the parts of the system we’ve personally worked on, particularly those we’ve most recently worked on. We downplay other parts of the system, including the impact of dependencies (e.g. libraries).

    The solution to the first of these observations is fairly widely known — one should rigorously profile a program before assuming one knows where it is spending the majority of its time. I deliberately say “rigorously profile” because people often confuse “I have profiled a program once” with “I have built up a good model of a program’s performance in a variety of situations”. Sometimes, a quick profiling job is adequate, but it can also mislead. Often it is necessary to profile a program with different inputs, sometimes on different machines or network configurations, and to use a variety of sampling and non-sampling approaches [1].

    However, the multiple solutions, and their inevitable trade-offs, to the second observation are, I believe, underappreciated. I tend to think that there are four main solutions:

    • Use a better algorithm.
    • Use a better data-structure.
    • Use a lower-level system.
    • Accept a less precise solution.

    In the rest of this post I’m going to go through each of these and give some suggestions for the trade-offs involved.

    Use a better algorithm

    Let’s imagine – and I’ve genuinely seen this happen! – that after careful profiling of a Python program, I find that I’m spending most of my time in a function which looks like this:

    def f1(l):
      while True:
        c = False
        for i in range(0, len(l) - 1):
          if l[i+1] < l[i]:
            t = l[i]
            l[i] = l[i+1]
            l[i+1] = t
            c = True
        if not c: return l

    It’s a bubble sort! At this point, many people will start guffawing, because it’s an obviously slow way of sorting elements. However, bubble sort has an often-forgotten advantage over many “better” algorithms: it runs in constant memory [2]. I could gamble that my program doesn’t need to use constant memory, but if I’m unsure, I can use an alternative algorithm which preserves this property. Let’s try a selection sort:

    def f2(l):
      for i in range(0, len(l) - 1):
        m = i
        for j in range(i + 1, len(l)):
          if l[j] < l[m]: m = j
        if m != i:
          t = l[i]
          l[i] = l[m]
          l[m] = t
      return l

    If I use this quick testing code:

    import random, time
    l = [random.random() for _ in range(1000)]
    before = time.time()
    l1 = f1(l[:])
    print(time.time() - before)
    before = time.time()
    l2 = f2(l[:])
    print(time.time() - before)

    and run it on CPython 3.11 on a Linux server I consistently get timings along the lines of:

    0.0643463134765625
    0.020025014877319336

    In other words, selection sort is about three times faster than bubble sort in my test.

    You don’t need me to tell you that selection sort isn’t the fastest possible sorting algorithm, but “fastest” is a more slippery concept than it first appears. For example, the selection sort algorithm above is faster than the bubble sort for random data, but the bubble sort is much faster for sorted data [3]. The relationship between inputs and algorithmic performance can be subtle. Famously, if you choose an unfortunate “pivot” when implementing quicksort, you’ll find that it is very non-quick (e.g. you can make it as slow on already-sorted data as the selection sort above).

    We can generalise from this that “use a better algorithm” requires understanding the wider context of your system and the nature of the algorithm you’re thinking of using. For example, I’ve often seen people conflate an algorithm’s best-case, average-case, and worst-case performance — but the differences between those three pieces of information can be vital when I’m optimising a program. Sometimes I might know something about my program (e.g. the nature of its inputs) that makes me confident that the worst case can’t happen, or I don’t consider the worst case to be a problem (e.g. its a batch job and no-one will notice occasional latency). But, generally, I care more about the worst case than the best case, and I select algorithms accordingly.

    It’s also not uncommon that algorithms that have good theoretical performance have poor real-world performance (big O notation can hide many sins). If in doubt, I try gradually more test data until I feel I have truly understood the practical consequences of different choices.

    It’s also easy to overlook complexity. Fundamentally, faster algorithms are faster because they observe that some steps in a calculation can be side-stepped. I can still remember the first time I read the description for timsort: the beauty of its algorithmic observations has stayed with me ever since. But verifying those observations is harder than we imagine — even timsort, created by one of the greatest programmers I have ever come across, had a subtle bug in it [4].

    When us mortals implement faster algorithms, they are often slightly incorrect, particularly when newly implemented, either producing wrong results or not having the expected performance characteristics [5]. For example, parallelising an algorithm can often lead to huge speedups, particularly as CPUs gain more cores, but how many of us understand the C11 memory model well enough to feel confident of the consequences or parallelisation?

    The combination of (in)correctness and the difficulty in understanding the context in which an algorithm is fast means that I frequently encourage people to start with a simple algorithm and only move to something “faster” if they really find they need to. Picking (and, if necessary, implementing) the right algorithm for the task at hand is a surprisingly difficult skill!

    Use a better data-structure

    Let’s imagine that I profile another program and find that I spend most of my time in the following function:

    def f3(l, e):
      for x in l:
        if x == e: return True
      return False

    It’s an existence check function! Optimising these can be quite interesting, because my choices will depend on how the lists passed to this function are used. I could change the list to a binary tree, for example. But if I can tell, as is not uncommon, that we repeatedly check for the existence of elements in a list that is never mutated after initial creation, I might be able to get away with a very simple data-structure: a sorted list. That might sound odd, because “sorted list” doesn’t sound like much of a data-structure, but that then allows me to do a binary search. For anything but the smallest lists [6], binary search is much quicker than the linear search above.

    Just as with “use a better algorithm”, “use a better data-structure” requires careful thought and measurement [7]. In general, while I often find it necessary to implement my own “better algorithms”, I rarely find it necessary to implement my own “better data-structures”. Partly this is laziness on my part, but it’s mostly because data-structures are more easily packaged in a library than better algorithms [8].

    There is an important tactical variant on “better data-structures” that is perhaps best thought of as “put your structs/classes on a diet”. If a program is allocating vast numbers of a given struct/class, the size of that struct/class in bytes can become a significant cost in its own right. When I was working on error recovery in grmtools, I found that simply reducing the most commonly allocated struct by 8 bytes in size improved total program performance by 5% — a trick that, from memory, I repeated twice!

    There are many similar tactics to this, for example reducing “pointer chasing” (typically by folding multiple structs/classes into one), encouraging memory locality and so on. However, while it’s easy to measure the size of a struct/class and how often it’s allocated, it’s difficult to measure the indirect impact of things like memory locality — I have heard such factors blamed for poor performance much more often than I have seen such factors proven as responsible for poor performance. In general, I only look to such factors when I’m getting desperate.

    Use a lower-level system

    A time-honoured tradition is to rewrite parts of a program in a lower-level programming language. Let’s rewrite our Python bubble sort into Rust:

    use std::cmp::PartialOrd;
    fn f1(l: &mut Vec) {
      loop {
        let mut c = false;
        for i in 0..l.len() - 1 {
          if l[i + 1] < l[i] {
            let t = l[i];
            l[i] = l[i + 1];
            l[i + 1] = t;
            c = true;
          }
        }
        if !c {
          return;
        }
      }
    }

    I mildly adopted my Python program from earlier to save out 1000 random floating point numbers, and added this testing code in Rust:

    use {env::args, fs::read_to_string, time::Instant};
    fn main() {
      let mut l = read_to_string(args().nth(1).unwrap())
        .unwrap()
        .lines()
        .map(|x| x.parse::().unwrap())
        .collect::>();
      let before = Instant::now();
      f1(&mut l);
      println!("{}", (Instant::now() - before).as_secs_f64());
    }

    }

    My Rust bubble sort runs in 0.001s, about 60x faster than the Python version. This looks like a great success for “rewrite in a lower-level programming language” — but you may have noticed that I titled this section “Use a lower-level system”.

    Instead of spending 15 minutes writing the Rust code, it would have been smarter of me to recognise that my Python bubble sort is likely to emphasise CPython’s (the most common implementation of Python) weaknesses. In particular, CPython will represent what I conceptually thought of as a list of floating point numbers as an array of pointers to individually heap-allocated Python objects. That representation has the virtue of generality but not efficiency.

    Although it’s often forgotten, CPython isn’t the only implementation of Python. Amongst the alternatives is PyPy, which just so happens to represent lists of floats as efficiently as Rust. Simply typing pypy instead of python speeds my bubble sort up by 4x! There are few changes I can make that give me such a big performance improvement for such little effort. That’s not to say that PyPy runs my program as fast as Rust (PyPy is still about 15x slower) but it may well be fast enough, which is what really matters.

    I have seen multiple organisations make the mistake of trying to solve performance problems by rewriting their software in lower-level programming languages, when they would have got sufficient benefit from working out how to run their existing software a little faster. There are often multiple things one can do here, from using different language implementations, to checking that you’ve got compiler optimisations turned on [9], to using faster libraries or databases, and so on. Sometimes rewriting in a lower-level programming language really is the right thing to do, but it is rarely a quick job, and it inevitably introduces a period of instability while bugs are shaken out of the new version.

    Accept a less precise solution

    A common problem we face is that we have n elements of something and we want to understand the best subset or ordering of those for our situation. Let’s imagine that I’ve implemented a compiler and 30 separate optimisation passes. I know that some optimisation passes are more effective if they run after other optimisation passes, but I don’t know what the most effective ordering of all the passes is.

    I could write a program to enumerate all the permutations of those 30 passes, run them against a benchmark suite I possess, and then select the fastest permutation. But if my benchmark suite takes 1 second to run then it will take roughly 282 years to evaluate all the possibilities — which is rather longer than the current age of the universe. Clearly I can’t wait that long for an answer: I can only run a subset of all the permutations. In situations such as this, I have to accept that I’ll never be able to know for sure what the best possible answer is: but, that said, I can at least make sure I end up with a better answer than not trying anything at all.

    There are various ways of tackling this but most boil down to local search. In essence, we define a metric (in our running example, how fast our benchmark suite runs) that allows us to compare two solutions (in our case, faster is better) and discard the worst. We then need a way of generating a neighbour solution to the one we already have, at which point we recalculate the metric and discard the worse of the old and new solution. After either a fixed time-limit, or if we can’t find solutions which improve our metric, we return the best solution we’ve found. The effectiveness of this simple technique (the core algorithm is a few lines of code) tends to stun newcomers, since the obvious problem of local optima seems like it should undermine the whole idea.

    As typically implemented, local search as I’ve outlined it above produces correct but possibly non-optimal solutions. Sometimes, however, we’re prepared to accept an answer which is less precise in the sense that it is possibly “incorrect”. By this I don’t mean that the program is buggy, but that the program may deliberately produce outputs that do not fully match what we would consider the “full and proper” answer.

    Exactly what constitutes “correct” varies from one situation to another. For example, fast inverse square root approximates multiplicative inverse: for situations such as games, its fast nearly-correct answer is a better trade-off than a slow definitely-correct answer. A Bloom filter can give false positives: accepting that possibility allows it to be exceptionally frugal with memory. JPEG image compression deliberately throws away some of an image’s fine details in order to make the image more compressible. Unlike other image compression approaches I cannot recover the original imagine perfectly from a JPEG, but by foregoing a little bit of image quality, I end up with much smaller files to transmit.

    I think that, in general, most programmers struggle to accept that correctness can sometimes be traded-off — personally, it offends a deep internal conviction of mine that programs should be correct. Probably because of that, I think the technique is used less often than it should be.

    Recently, though, we’ve become much more willing to accept incorrect answers thanks to the explosion of ML (Machine Learning). Whereas local search requires us to explicitly state how to create new solutions, ML is trained on previous data, and then generates new solutions from that data. This can be a very powerful technique, but ML’s inevitable “hallucinations” are really just a form of incorrectness.

    We can thus see that there are two different ways of accepting imprecise solutions: possibly non-optimal; and possibly incorrect. I’ve come to realise that many people think they’re the same thing, but possible incorrectness more often causes problems. I might be happy trading off a bit of image-quality for better compression, but if an ML system rewrites my code and leaves off a “not” I’m unhappy. My rule of thumb is that unless you are convinced you can tolerate incorrectness, you’re best off assuming that you can’t.

    Summary

    I’ve listed the four optimisation approaches above in the frequency with which I’ve seen them used (from most to least used).

    It will probably not surprise you that my least favourite approach is “rewrite in a lower-level programming language”, in the sense that it tends to offer the poorest ratio of improvement/cost. That doesn’t mean that it’s always the wrong approach, but we tend to reach for it before we’ve adequately considered cheaper alternatives. In contrast, I think that until recently we have too rarely reached for “accept a less precise solution”, though the ML explosion has rapidly changed that.

    Personally, when I’m trying to optimise a program I tend to reach for the simplest tricks first. One thing that I’ve found surprises people is how often my first attempt at optimisation will be to hunt for places to use hashmaps — only rarely do I go hunting for exotic data-structures to use. I less often turn to clever algorithms. Of those clever algorithms I tend to implement myself, I suspect that binary search is the one I use the most often, and I probably do so at most once or twice a year — each time I implement it, I have to look up the correct way to do so [10]!

    Ultimately, having written this post, I’ve come to realise that there are three lessons that cut across all of the approaches.

    First, when correctness can be sacrificed for performance, it’s a powerful technique — but we often sacrifice correctness for performance unintentionally. When we need to optimise a program, it’s best to use the least complex optimisation that will give us the performance we want, because that’s likely to introduce the fewest bugs.

    Second, human time matters. Because us programmers enjoy complexity so much, it’s tempting for us to reach for the complex optimisations too soon. Even if they succeed in improving performance – which they often don’t! – they tend to consume much more time than is necessary for the performance improvement we needed.

    Third, I think that breadth of optimisation knowledge is more important than depth of optimisation knowledge. Within each of the approaches I’ve listed in this post I have a couple of tricks that I regularly deploy. That has helped give me a reasonable intuition about what the most appropriate overall approach to my current performance woes might be, even if I don’t know the specifics.

    Acknowledgements: Thanks to Carl Friedrich Bolz-Tereick, and Jake Hughes for comments.

    Update (2023-11-14): My original phrasing of a Bloom filter could be read in a way that seemed to be a contradiction. I’ve tweaked the phrasing to avoid this.

    Designing Data Intensive Applications (DDIA) Book

    Mike's Notes

    Below is a copy of a review of the book Designing Data Intensive Applications, written by Martin Kleppmann and published in 2017 by O'Reilly. The review is by Murat Demirbas. The original review has links and excellent diagrams.

    Martin Kleppmann is an Associate Professor at the University of Cambridge, UK, working on local-first collaboration software and distributed systems security.

    Murat Demirbas is a Computer Science Professor at Buffalo SUNY in New York State, USA.

    Both have interesting and useful blogs on computer engineering. Martin has many recorded conference presentations.

    Resources

    References

    • Designing Data Intensive Applications

    Repository

    • Home > Ajabbi Research > Library >
    • Home > Handbook > 

    Last Updated

    17/05/2025

    Designing Data Intensive Applications (DDIA) Book

    By: Murat Demirbas
    Metadata Blog: 6/8/2024

    We started reading this book as part of Alex Petrov's book club. We just got started, so you can join us, by joining the discord channel above. We meet Wednesday's 11am Eastern Time. 

    Previously we had read transaction processing book by Grey and Reuters. This page links to my summaries of that book.

    Chp 1. Reliable, Scalable, and Maintainable Applications

    I love the diagrams opening each chapter. Beautiful!

    The first chapter consists of warm up stuff. It talks about the definitions of reliabilty, scalability, and maintainability. It is still engaging, because  it is written with an educator and technical blogger voice, rather than a dry academic voice.

    This book came out on 2017. Martin is working on the new version. So if you have comments for things to focus on for the new version, it would be helpful to collect them in a document and email it to Martin. For example, I am curious about how the below paragraph from the Preface will get revised with 8 more years of hindsight:

    "Sometimes, when discussing scalable data systems, people make comments along the lines of, “You’re not Google or Amazon. Stop worrying about scale and just use a relational database.” There is truth in that statement: building for scale that you don’t need is wasted effort and may lock you into an inflexible design. In effect, it is a form of premature optimization. However, it’s also important to choose the right tool for the job, and different technologies each have their own strengths and weaknesses. As we shall see, relational databases are important but not the final word on dealing with data."

    Chp 2. Data Models and Query Languages

    The choice of data model significantly impacts the capabilities of software above it. This chapter provides a really nice and objective review of the landscape. 

    Relational Model and SQL

    The relational model, proposed by Edgar Codd in 1970, forms the basis of SQL. It organizes data into relations (tables) containing tuples (rows). Relational databases were first deployed for business data processing on 1960s-70s mainframes. They simplified data management by hiding implementation details behind a clean interface. By the mid-1980s, relational database management systems (RDBMSes) and SQL became the defacto tools for data storage and querying.

    Various models challenged the relational model's dominance:

    • Network and hierarchical models (1970s-early 1980s)
    • Object databases (late 1980s-early 1990s)
    • XML databases (early 2000s)
    • NoSQL/Document databases (2010s)

    Document Databases and NoSQL

    NoSQL arose from the need for greater scalability, usability (especially programmability by developers), and desire for more flexible schemas.

    Let's double-click on usability/programmability. If data is stored in relational tables, an awkward translation layer is required between the objects in the application code and the database model of tables, rows, and columns. Object-relational mapping (ORM) frameworks try to alleviate this impedance mismatch, and reduce the amount of boilerplate code required for this translation layer, but they can’t completely hide the differences between the two models.

    To address this issues, document databases store data in a JSON-like format, and provide:

    • Reduced "impedance mismatch" between application code and storage layer
    • Better data locality
    • Schema flexibility

    However, they face challenges with complex joins and many-to-many relationships.

    Relational vs. Document Model

    The choice between relational and document models depends on the application's needs:

    • Document model excels for one-to-many relationships and tree-like structures
    • Relational model is better for complex joins and many-to-many relationships

    Or, dually, Document databases might lead to data duplication, while relational databases may require "shredding" document-like structures across multiple tables.

    The JSON representation has better locality than the multi-table schema in Figure 2-1. If you want to fetch a profile in the relational example, you need to either perform multiple queries (query each table by user_id) or perform a messy multi-way join between the users table and its subordinate tables. In the JSON representation, all the relevant information is in one place, and one query is sufficient.

    While document databases are known for data locality, some relational databases also offer this feature. For example, Google Spanner's tables allow nesting of related data and the column-family concept in the Bigtable data model (used in Cassandra and HBase) also aim to improve locality.

    Query languages also vary between models. SQL uses an English-sentence-style syntax, while document databases often use JSON-based query languages. For instance, MongoDB's aggregation pipeline language is similar in expressiveness to a subset of SQL but uses a JSON-based syntax.

    UPDATE (December 2024): Here is the index for the DDIA chapters after having gone through the book!

    Ajabbi modules

    Mike's Notes

    My thoughts on using modules.

    Resources

    References

    • Reference

    Repository

    • Home > Ajabbi Research > Library >
    • Home > Handbook > 

    Last Updated

    17/05/2025

    Article

    By: Mike Peters
    On a Sandy Beach: 23/01/2025

    Mike is the inventor and architect of Pipi and the founder of Ajabbi.

    I'm working on getting Pipi 9 to create an application for the first customer. It's going well, and I'm learning along the way. The application allows the publishing of enormous, highly structured static websites from a content database and templates, similar to what Adobe Framemaker does for technical print publications, catalogues, manuals, etc.

    Pipi 4 CMS (Content Management System) created a 93,000-page static website in a similar way years ago, in 2008.

    This is the opposite of Squarespace, which is excellent at creating smaller websites with highly customisable web pages.

    WordPress is also excellent, but it is not suitable for hosting big enterprise systems.

    It's got me thinking. The Ajabbi SaaS applications will all be open-sourced and available on GitHub without restrictions. But what does that really mean?

    Most applications consist of reusable modules and user-contributed plugins, so sharing them on GitHub as modules and plugins would make them easier to document and organise and give users more options for combining them to create whatever they want.

    It would also simplify the paid hosting by usage option, which allows any modules to be added to the enterprise account. Despite the growing complexity, Pipi 9 would automatically handle integration issues, updates, security, etc and would be cheaper than the costs of DIY.

    For example, the future aviation application includes these modules:

    • Aircraft
    • Airport
    • Airspace
    • Cargo
    • Flights

    They could also be made more granular (chopped into smaller parts). My idea was for Pipi 9 to package the modules in a zip file, similar to OpenERP or Odoo, as they are now called. Each would contain a database, code, processes, documentation, translated strings, web pages, CSS, etc.

    Each industry application is a bundle of modules.

    Creating a simple free website in 2025

    Mike's Notes

    Here are some further notes by Matt Sayar on using Cloudflare pages to host a static website. The original article on his website has some pictures and more article links.

    Resources

    References

    • Reference

    Repository

    • Home > Ajabbi Research > Library >
    • Home > Handbook > 

    Last Updated

    17/05/2025

    Creating a simple free website in 2025

    By: Matt Sayar
    https://mattsayar.com: 8 January 2025

    I've owned mattsayar.com since December 2010. According to archive.org, it appears I started experimenting with Wordpress sometime in May 2011, but then switched to using Tumblr as my host in 2012.

    Honestly, I still like this Tumblr design

    For years afterwards, this site was just a simple index.html on AWS S3 that said "Welcome to my website." There's a lot to be proud of there: free hosting, cloud-native, small footprint! But then I decided to start writing more publicly.

    This post will focus on how I created the website you see today.

    Since I work in security, my first order of business was to make my site secure. I remember Troy Hunt created httpsiseasy.com, so I started there. He made those videos in mid-2018 so some of the UIs are outdated, but Cloudflare is still a top-tier service provider.

    Namecheap, my domain registrar, was also my DNS provider, so I switched that over to Cloudflare. I had trouble finding the right DNS config in Namecheap's UI because I immediately dove into the "Advanced DNS" settings, only for it be on the main page of my domain name's settings.

    At that point I was struggling to get a secure connection between Cloudflare and my S3 bucket. I wanted to use the "Full (Strict)" SSL/TLS encryption mode because it's the most secure, but it wouldn't work. The only thing that WOULD work is the "Flexible" setting, and the Cloudflare UI does a great job of representing what that means visually.

    I added those little blue notes there showing what the Flexible setting means. I want it to be secure end-to-end!

    I did some troubleshooting with ChatGPT and came to the conclusion that I would need to use Amazon's Cloudfront to ensure the connection between Cloudflare and AWS is secure. I'm not looking to complicate things, so decided against that. Good thing Cloudflare has Cloudflare Pages with a generous free tier!

    I could've uploaded my humble index.html to Cloudflare Pages directly, but I saw it also supports GitHub. Since I knew beforehand that I wanted to use Publii as my content management system (CMS), I followed their excellent docs to create a GitHub repo that would host my site's files. 

    From there, Cloudflare Pages has an easy setting to set mattsayar.com as the custom domain name to replace the ugly default Cloudflare Pages domain name. When you configure that setting, Cloudflare automatically creates the proper DNS records for you!

    Like I mentioned a minute ago, I had an open tab for Publii on my laptop for a long, long time and figured it was worth giving it a shot. I first heard about it on HackerNews a couple years ago.

    Publii is a desktop app that lets you pick a design theme and write posts/pages which you can publish by syncing your website with a hosting provider. It's pretty noob-friendly after you configure it. I set Publii up to publish to GitHub, whereupon Cloudflare Pages recognizes my GitHub repo is updated, and it will then deploy my new updates to all of Cloudflare's edge nodes. 

    Apparently these static (non-dymanic) site CMSs are a dime a dozen, and I didn't want to spend a large amount of time comparing the pros and cons of any of them, so I just picked that one. I worry that one day I may want more dynamic features, but that's a problem for Future Me.

    And that's what creating a simple, free static website looks like in 2025!

    Is this the easiest way to do it? Probably not. Sites like Wix and Squarespace exist so that you don't even have to know what "DNS" or "HTTPS" means, but you pay for those services. I'm thankful open source projects like Publii and various free hosting sites exist.

    Why does Cloudflare Pages have such a generous Free tier?

    Mike's Notes

    Several sites offer free hosting for static websites.

    They include:

    • Cloudflare
    • GitHub
    • GitLab
    • Netlify
    • AWS S3
    The article below came in a few days ago on how to make use of Cloudflare. The original post has links in the article.

    Resources

    References

    • Reference

    Repository

    • Home > Ajabbi Research > Library >
    • Home > Handbook > 

    Last Updated

    17/05/2025

    Why does Cloudflare Pages have such a generous Free tier?

    By: Matt Sayar
    https://matsayar.com: 15 January 2025

    This site is hosted with Cloudflare Pages and I'm really happy with it. When I explored how to create a site like mine in 2025, I wondered why there's an abundance of good, free hosting these days. Years ago, you'd have to pay for hosting, but now there's tons of sites with generous free tiers like GitHub Pages, GitLab Pages, Netlify, etc.

    There are various types of usage limits across the platforms, but the biggest one to worry about is bandwidth. Nothing can make your heartrate faster than realizing your site is going viral and you either have to foot the bill or your site gets hugged to death. I gathered some limits from various services here.

    • Cloudflare Pages
      • Unlimited
      • Just don't host Netflix
    • GitHub Pages
      • Soft 100 GBs
      • "Soft" = probably fine if you go viral on Reddit sometimes
    • GitLab Pages
      • X,000 requests/min
      • Lots of nuances, somewhat confusing
    • Netlify
      • 100GB
      • Pay for more
    • AWS S3
      • 100 GB
      • Credit card required, just in case... but apparently Amazon is very forgiving of accidental overages

    The platforms generally say your site shouldn't be more than ~1GB in size and less than some tens of thousands of files. This site in its nascency is about 15MB and <150 files. I don't plan to start posting RAW photo galleries, so if I start hitting those limits, please be concerned for my health and safety.

    So why is Cloudflare Pages' bandwidth unlimited?

    Why indeed. Strategically, Cloudflare offering unlimited bandwidth for small static sites like mine fits in with its other benevolent services like 1.1.1.1 (that domain lol) and free DDOS protection.

    Cloudflare made a decision early in our history that we wanted to make security tools as widely available as possible. This meant that we provided many tools for free, or at minimal cost, to best limit the impact and effectiveness of a wide range of cyberattacks. - Matthew Prince, Cloudflare Co-Founder and CEO

    But I want to think of more practical reasons. First, a static website is so lightweight and easy to serve up that it's barely a blip on the radar. For example, the page you're reading now is ~2.2MB, which is in line with typical page weights of ~2.7MB these days. With Cloudflare's ubiquitous network, caching, and optimization, that's a small lift. My site ain't exactly Netflix.

    Second, companies like Cloudflare benefit from a fast, secure internet. If the internet is fast and reliable, more people will want to use it. The more people that want to use it, the more companies that offer their services on the internet. The more companies that offer services on the internet, the more likely they'll need to buy security products. Oh look, Cloudflare happens to have a suite of security products for sale! They flywheel spins...

    Third, now that I’m familiar with Cloudflare’s slick UI, I’m going to think favorably about it in the future if my boss ever asks me about their products. I took zero risk trying it out, and now that I have a favorable impression, I'm basically contributing to grassroots word-of-mouth marketing with this very article. Additionally, there's plenty of "Upgrade to Pro" buttons sprinkled about. It's the freemium model at work.

    What does Cloudflare say?

    Now that I have my practical reasons, I'm curious what Cloudflare officially says. I couldn't find anything specifically in the Cloudflare Pages docs, or anywhere else! Neither the beta announcement or the GA announcement have the word "bandwidth" on the page.

    Update: shubhamjain on HN found a great quote from Matt Prince that explains it's about data and scale. And xd1936 helpfully found the official comment that evaded my googling.

    I don't know anybody important enough to get me an official comment, so I suppose I just have to rely on my intuition. Fortunately, I don't have all my eggs in one basket, since my site is partially hosted on GitHub. Thanks to that diversification, if Cloudflare decides to change their mind someday, I've got options!