Everything About Bitflags

23 min read
web-devbitflagsjavascript5 more...

When I was younger and I was involved in reverse engineering communities and systems programming, there was this concept called bit flags. They were a standard way of storing a pack of true or false values in ... actually a single value - a function parameter, local variable or entry in some configuration. I found nothing fascinating about that back then and just used it on a regular basis, as every other engineer was.

Long time after that and after shifting my focus to web development I just realized this concept exists and is widely used, but for some reason not in web dev. Several years passed but still, I have not seen even a single usage - neither in the frontend code nor in the backends.

Today I am going to walk through the whole idea and the pros and cons of this approach, the approach that allows storing up to 32 eventual boolean values in a single integer.

The example of a hassle coming within traditional approaches to store flags in web development

There may be a lot more issues with implementing any kind of flags system than it may seem to be. Including not only technical considerations or DX-affecting code smells but also things that are bad for business. For instance:

  • JSON-based flags systems in possibly making excessive bandwidth (even if using GraphQL) and computing power usage (which also increases infrastructure costs) or growing latencies when listing the flags over the network for applications with enormous network traffic where performance is absolutely critical.

  • JSON-based flags systems stored without end to end operation safety, creating pain points between application and database layers like discrepancies in areas such as:

    • Strong types across many projects in monorepo and multirepo in different programming languages and tech.
    • Formatting, stringifying, parsing, charset, possibly
  • High throughput in-memory processing systems with large volumes of data, causing RAM spikes with immutability patterns - huge data structures being copied over and over

  • Inefficient, hastily designed unstructured flags systems at the beginning of the project's development phase. Without scalability capabilities, they increase the costs of modernizing quick-to-become legacy and problematic infrastructures that create problems "out of nowhere" because "it worked yesterday," potentially delaying previously planned business strategies.

  • Defining flags as separate columns in the database modeling phases, ending up in a mess such as 20 columns named is_something_enabled. While this approach is quite performant and should be the go-to solution if no more than ~4 flags are needed, the growing number of booleans to store will create a mess

For educational purposes of this article while reading further sections - let's imagine building a rich text editor with user preferences being saved server-side (in the remote database). Assume the flags are for three things:

  1. If the default browser's spell checker is being disabled (name of the flag: BUILTIN_SPELLCHECK_DISABLED)
  2. If the third-party spell checkers is being disabled (name of the flag: THIRD_PARTY_SPELLCHECK_DISABLED)
  3. If the text area should be resizable (if the bottom right arrow could be used to adjust the height of the input by the user) (name of the flag: RESIZABLE_TEXT_AREA)

What are bit flags?

Imagine having a row of light switches. Each switch can be either ON or OFF, representing a state between true and false, a state between 1 and 0


Reference drawing explaining the light switches assumed concept and its representation in actual binary number and bits.

This "row of light switches" actually has its own name in the computer science - it is called binary number, and every particular switch is a bit

The example of a bit flags implementation

Let's define the flags as a plain object with numbers

const EditorFlags = {
  NONE:                            0,       // 00000000 (dec: 0, hex: 0x0)
  BUILTIN_SPELLCHECK_DISABLED:     1 << 0,  // 00000001 (dec: 1, hex: 0x1)
  THIRD_PARTY_SPELLCHECK_DISABLED: 1 << 1,  // 00000010 (dec: 2, hex: 0x2)
  RESIZABLE_TEXT_AREA:             1 << 2,  // 00000100 (dec: 4, hex: 0x4)
}

Take a while to look at it, especially the comments to every line. It can be observed that each of the defined flags converted to a binary number and placed under each other has exactly one bit toggled to 1 (every light switch) occupying a different "perceived column".


Reference drawing explaining the uniqueness of bits occupied at different positions when comparing all our defined flags.

OR operator for composing the flags into one value

These unique positions create a possibility to "merge" the flags into one value. In this case, the bitwise OR (|) operator will do the trick, by "summing up" the columns.


Reference drawing explaining the light switches assumed concept and its representation in actual binary number and bits.

JavaScript/TypeScript example usage

In JavaScript/TypeScript the bitwise OR (|) operation would look like

const combinedFlags =
  EditorFlags.BUILTIN_SPELLCHECK_DISABLED |
  EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED |
  EditorFlags.RESIZABLE_TEXT_AREA

console.log(combinedFlags) // 7

We get 000111 as the binary number result, 7 as decimal result and 0x7 as a hexadecimal one. This single number represents all of our flags combined. It can be saved anywhere, remote database, browser storages, in-memory states of the application or configuration files living directly in the filesystem.

PostgreSQL example storage and operations usage

  1. Create the table

    CREATE TABLE user_editor_preferences (
      id SERIAL PRIMARY KEY,
      user_id INTEGER NOT NULL UNIQUE,
      editor_flags INTEGER DEFAULT 0,  -- Stores bitflags for editor settings
      last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
  2. Insert the data

    • Manually just push the integer after bitwise operations on the application level
      INSERT INTO user_editor_preferences (user_id, editor_flags) VALUES (2, 7)
      -- EditorFlags.BUILTIN_SPELLCHECK_DISABLED ─────────────┬──────────────┘
      -- EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED ─┬───────┘
      -- EditorFlags.RESIZABLE_TEXT_AREA ─────────────┘
      
    • Perform bitwise << and | on the query level:
      INSERT INTO user_editor_preferences (user_id, editor_flags) 
      VALUES (1, (1 << 0) | (1 << 1) | (1 << 2));
      --         └───┬──┘   └──┬───┘   └───┬──┘
      --             │         │           │
      --             │         │           └─ 1 << 2 = 4 (0x4)
      --             │         │              RESIZABLE_TEXT_AREA 
      --             │         │                                                
      --             │         └─ 1 << 1 = 2 (0x2)                             
      --             │            THIRD_PARTY_SPELLCHECK_DISABLED 
      --             │                                                  
      --             └─ 1 << 0 = 1 (0x1)                               
      --                BUILTIN_SPELLCHECK_DISABLED             
      

AND operator for checking if certain flags are enabled

With the number containing all the flags stored somewhere, a way is needed to actually check if certain flags are enabled within this number. For this case, the AND operator (&) would be the way to go.

The operator scans each bit of the two binary numbers listed in the expression and returns a new number whose bits are set to 1 (light switches are set to ON) only if bits at the same "perceived columns" are toggled in the input numbers


Reference drawing explaining the bitwise AND operator

JavaScript/TypeScript example usage

In JavaScript/TypeScript the bitwise AND (&) operation would look like

const andOperationResult = combinedFlags & EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED

console.log(andOperationResult) // 2

Looking at the code above and Fig. 4, the result does not tell much and it seems that the number is exactly equal to the THIRD_PARTY_SPELLCHECK_DISABLED which was just passed as the input binary number.

There's actually a reason for that. Getting back to the section explaining how the bitwise AND operator (&) works, it can be noticed that this is correct behavior and that comparing flags like this will always return the right-hand side of the operation if the flag being checked is present in the combined flags

Having that in mind, it's a perfect case for creating a helper factory function checking for equality to actually see if the particular flag is inside the stored number with some flags (all flags in this example) combined

export function bitflag(input: number) {
  return {
    has: (flag: number) => (input & flag) === flag; 
  }
}

and using it

const hasThirdPartySpellcheckingDisabled =
  bitflag(combinedFlags).has(EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED)

console.log(hasThirdPartySpellcheckingDisabled) // true

PostgreSQL example operations usage

The following query will return all rows where editor_flags has the THIRD_PARTY_SPELLCHECK_DISABLED

SELECT * FROM user_editor_preferences WHERE editor_flags & 2 = 2
-- BitFlags.THIRD_PARTY_SPELLCHECK_DISABLED ───────────────┴───┘ 

AND plus NOT for removing the flags

In order to implement remove functionality for the bitflags, clever use of the NOT operator (~) and the AND operator (&) afterwards is needed.


Reference drawing explaining the bitwise AND + NOT operators

Analyzing the drawing, first the flag to be removed is obtained - in this case EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED, then all its bits (all the perceived columns) are flipped to the contrasting value (if it's 0, it will flip to 1, if it's 1 it will flip to 0)

After that, already knowing how the AND operator (&) works, the combinedFlags is used as the left-hand side and the mask as the right-hand side to perform the operation.

The returned value from the operation will be exact binary number having all our flags combined without EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED

JavaScript/TypeScript example usage

Having that in mind, we can modify helper factory function from previous section to:

export function bitflag(input: number) {
  return {
    has: (flag: number) => (input & flag) === flag; 
    remove: (flag: number) => input & ~flag;
  }
}

and use it as follows:

const flagsWithoutThirdPartySpellcheckDisabled =
  bitflag(combinedFlags).remove(EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED)

console.log(flagsWithoutThirdPartySpellcheckDisabled)
//          └───── equivalent of:
//                   000101
//                   BUILTIN_SPELLCHECK_DISABLED | RESIZABLE_TEXT_AREA
//                   1 | 4

PostgreSQL example storage and operations usage

Insert the data

  1. Update the value directly with bitwise operations done on the application level
    INSERT INTO user_editor_preferences (user_id, editor_flags) VALUES (2, 5)
    -- EditorFlags.BUILTIN_SPELLCHECK_DISABLED ─────┬──────────────────────┤
    -- EditorFlags.RESIZABLE_TEXT_AREA ─────────────┘                      │
    --                                                           calculated in JS/TS
    --                                                           on application level
    
  2. Perform bitwise <<, ~ and & on the query level:
    UPDATE user_editor_preferences 
    SET editor_flags = editor_flags & ~(1 << 1) WHERE user_id = 1
    --                 │               └───┬──┘
    --                 │                   │
    --                 │                   └─ THIRD_PARTY_SPELLCHECK_DISABLED (2)
    --                 │               
    --                 │              
    --                 │    
    --               Current: 7
    --                 BUILTIN_SPELLCHECK_DISABLED (1) 
    --                 THIRD_PARTY_SPELLCHECK_DISABLED (2) 
    --                 RESIZABLE_TEXT_AREA (4) 
    --                                                          
    --                  Result: Updated column with editor_flags = 5         
    --                    BUILTIN_SPELLCHECK_DISABLED (1)
    --                    RESIZABLE_TEXT_AREA (4)
    

XOR for toggling the flags

While previously described operators cover most of the functionality needed, there's a common "bitwise-native" way to implement a QoL flags toggling mechanism. For that, the XOR operator (^) will be used.

To perform such an operation, the number where flags are combined is needed as the left-hand side and the flag to toggle as the right-hand side - it will be treated as some kind of "mask", similar to the mask created by the NOT operator (~).

After executing the logic, the bit toggled in the mask will be flipped in the number where flags are combined. And this can be repeated indefinitely to perform toggling (ON/OFF) on the particular flag


Reference drawing explaining the bitwise XOR operator

JavaScript/TypeScript example usage

Having that in mind, we can modify helper factory function from previous sections once again:

export function bitflag(input: number) {
  return {
    has: (flag: number) => (input & flag) === flag; 
    remove: (flag: number) => input & ~flag;
    toggle: (flag: number) => input ^ flag;
  }
}

and use it as follows:

const flagsWithoutThirdPartySpellcheckDisabled =
  bitflag(combinedFlags).toggle(EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED)

console.log(flagsWithoutThirdPartySpellcheckDisabled)
//          └───── equivalent of:
//                   000101
//                   BUILTIN_SPELLCHECK_DISABLED | RESIZABLE_TEXT_AREA
//                   1 | 4
console.log(bitflag(flagsWithoutThirdPartySpellcheckDisabled)
    .toggle(EditorFlags.THIRD_PARTY_SPELLCHECK_DISABLED))
//        equivalent of: ───────────────────────────────┘
//                   000111
//                   BUILTIN_SPELLCHECK_DISABLED | THIRD_PARTY_SPELLCHECK_DISABLED | RESIZABLE_TEXT_AREA
//                   1 | 2 | 4

Going further

The text editor implementation and bit flags although being a perfectly simple example to showcase the functionality of bit flags is not a case that requires the best performance and serious optimization considerations.

Bit flags come especially handy in network-heavy operations and high-frequency in-memory data processing, basically anything that will potentially result in excessive computing or money cost rising in pair with the growing volume of data and frequency of the accesses of such data. For instance:

  1. Permission systems and roles (authorization).
  2. Comprehensive and heavy logging/telemetry/analytics mechanisms.
  3. Game development - in particular implementing multiplayer and creating high-throughput game servers or checking loads of states of game objects/entities per second.

Implementing bit flags mechanisms in your codebase

There's loads of theoretical knowledge in the article; quickly hopping into real world codebases introducing these concepts can be cumbersome, especially for developers encountering this for the first time.

Ready to copy-and-paste utility factory function

The factory function that was mentioned in particular sections regarding specific operations is a perfect "bridge" between the article and the actual code. In its final form, it would look like this

export function bitflag(value = 0) {
  const combine = (...flags) => flags.reduce((acc, flag) => acc | flag, 0)
  return {
    has: (...flags) =>
      flags.length === 0
        ? false
        : (value & combine(...flags)) === combine(...flags),
    hasAny: (...flags) =>
      flags.length === 0 ? false : (value & combine(...flags)) !== 0,
    hasExact: (...flags) =>
      flags.length === 0 ? value === 0 : value === combine(...flags),
    add: (...flags) => (flags.length === 0 ? value : value | combine(...flags)),
    remove: (...flags) =>
      flags.length === 0 ? value : value & ~combine(...flags),
    toggle: (...flags) =>
      flags.length === 0 ? value : value ^ combine(...flags),
    clear: () => 0,
    value,
    valueOf: () => value,
    toString: () => value.toString()
  }
}

This minimal mixed-pardagim (Object Oriented Programming + Functional Programming) implementation can be copied and pasted to any codebase, for instance you can create the files:

  • /lib/bitflag.ts in Next.js projects and imported both in RSC or client context
  • /shared/utils/bitflag.ts for Nuxt.js projects and imported both in Nitro backend and Vue frontend

then define the flags somewhere in /constants/ directory of your choice

const MyFlags = {
  NONE = 0,
  MY_FIRST_FLAG = 1 << 0,
  // ...
  MY_NINTH_FLAG = 1 << 8
}

and use it as follows

import { bitflag } from "./mycodebase/utils/bitflags"
import { MyFlags } from "./mycodebase/utils/constants"

bitflag(MyFlags.MY_FIRST_FLAG).add(MY_NINTH_FLAG)

Ready to install library for managing and operating on bitflags.

While writing this article, I came up with an idea of extracting the utility factory function not only to the ready-to-copy section in this article but also as a separate library, making it convenient for more advanced usages, e.g. writing highly reliable applications where end-to-end type safety is needed.

Features of the bitf library
  • Type safety via Tagged1 types.
  • Lightweight and fast, almost native bitwise performance with minimal abstraction layer.
  • No runtime dependencies.
  • Robust and ready-to-use on production. 100% test coverage
  • .describe() iterator for better debugging and visualization of the flags.
pnpm add bitf
bun add bitf
yarn add bitf
npm install bitf
Library implementation example
import { type Bitflag, bitflag, defineBitflags } from "bitf";

// This should probably live in a file shared between frontend/backend contexts
const Toppings = defineBitflags({
  CHEESE: 1 << 0,
  PEPPERONI: 1 << 1,
  MUSHROOMS: 1 << 2,
  OREGANO: 1 << 3,
  PINEAPPLE: 1 << 4,
  BACON: 1 << 5,
  HAM: 1 << 6,
});

// Can be mapped on frontend using InferBitflagsDefinitions<typeof Toppings> and .describe() function
type PizzaOrderPreferences = Readonly<{
  desiredSize: "small" | "medium" | "large";
  toppingsToAdd: Bitflag;
  toppingsToRemove: Bitflag;
}>;

export async function configurePizzaOrder({
  desiredSize,
  toppingsToAdd,
  toppingsToRemove,
}: PizzaOrderPreferences) {
  if (bitflag(toppingsToRemove).has(Toppings.CHEESE))
    throw new Error("Cheese is always included in our pizzas!");

  const defaultPizza = bitflag().add(Toppings.CHEESE, Toppings.PEPPERONI);
  const pizzaAfterRemoval = processToppingsRemoval(
    defaultPizza,
    toppingsToRemove
  );

  validateMeatAddition(pizzaAfterRemoval, toppingsToAdd);

  // ... some additional logic like checking the toppings availability in the restaurant inventory
  // ... some additional logging using the .describe() function for comprehensive info

  const finalPizza = bitflag(pizzaAfterRemoval).add(toppingsToAdd);

  return {
    size: desiredSize,
    pizza: finalPizza,
    metadata: {
      hawaiianPizzaDiscount: bitflag(finalPizza).hasExact(
        Toppings.CHEESE,
        Toppings.HAM,
        Toppings.PINEAPPLE
      ),
    },
  };
}

function processToppingsRemoval(
  currentPizza: Bitflag,
  toppingsToRemove: Bitflag
) {
  if (toppingsToRemove) return bitflag(currentPizza).remove(toppingsToRemove);
  return currentPizza;
}

function validateMeatAddition(currentPizza: Bitflag, toppingsToAdd: Bitflag) {
  const currentHasMeat = bitflag(currentPizza).hasAny(
    Toppings.PEPPERONI,
    Toppings.BACON,
    Toppings.HAM
  );

  const requestingMeat = bitflag(toppingsToAdd).hasAny(
    Toppings.PEPPERONI,
    Toppings.BACON,
    Toppings.HAM
  );

  if (currentHasMeat && requestingMeat)
    throw new Error("Only one type of meat is allowed per pizza!");
}
Show more

To learn more about the library, check out its GitHub repository!

Benchmarks

The performance takes on at the beginning of the article are serious considerations, that is also a good reason behind benchmarking the common aproaches to managing all sort of flags in the application's internal systems.

Application level computing benchmarks

The benchmarks are done in scope of bitf library and custom-made JSON-based flags implementation that aims to be as close as possible to the common patterns of creating such abstractions in web development - with immutability, spread operator and Object methods.

Node.js (v8)

Tested on Node.js v24.3.0, with process.versions printing:

{"node":"24.3.0","acorn":"8.15.0","ada":"3.2.4","amaro":"1.1.0","ares":"1.34.5","brotli":"1.1.0","cjs_module_lexer":"2.1.0","cldr":"47.0","icu":"77.1","llhttp":"9.3.0","modules":"137","napi":"10","nbytes":"0.1.1","ncrypto":"0.0.1","nghttp2":"1.66.0","openssl":"3.0.16","simdjson":"3.13.0","simdutf":"6.4.0","sqlite":"3.50.1","tz":"2025b","undici":"7.10.0","unicode":"16.0","uv":"1.51.0","uvwasi":"0.0.21","v8":"13.6.233.10-node.18","zlib":"1.3.1-470d3a2","zstd":"1.5.7"}


Bun (JavaScriptCore)

Tested on Bun 1.2.18 with process.versions printing:

{"node":"24.3.0","bun":"1.2.18","boringssl":"29a2cd359458c9384694b75456026e4b57e3e567","openssl":"1.1.0","libarchive":"898dc8319355b7e985f68a9819f182aaed61b53a","mimalloc":"4c283af60cdae205df5a872530c77e2a6a307d43","picohttpparser":"066d2b1e9ab820703db0837a7255d92d30f0c9f5","uwebsockets":"0d4089ea7c48d339e87cc48f1871aeee745d8112","webkit":"29bbdff0f94f362891f8e007ae2a73f9bc3e66d3","zig":"0.14.1","zlib":"886098f3f339617b4243b286f5ed364b9989e245","tinycc":"ab631362d839333660a265d3084d8ff060b96753","lolhtml":"8d4c273ded322193d017042d1f48df2766b0f88b","ares":"d1722e6e8acaf10eb73fa995798a9cd421d9f85e","libdeflate":"dc76454a39e7e83b68c3704b6e3784654f8d5ac5","usockets":"0d4089ea7c48d339e87cc48f1871aeee745d8112","lshpack":"3d0f1fc1d6e66a642e7a98c55deb38aa986eb4b0","zstd":"794ea1b0afca0f020f4e57b6732332231fb23c70","v8":"13.6.233.10-node.18","uv":"1.48.0","napi":"10","icu":"74.2","unicode":"15.1","modules":"137"}

Database level storage / network bandwidth benchmarks

The following benchmarks are theoretical as seeding a database with such data would be quite complex; however, simple math for the scenario is still a reliable way to calculate these.

Numeric assumptions for a mid-scale SaaS project management platform with:

  • 300,000 users (typical for successful B2B SaaS)
  • 31 permission flags per user (maximum for JavaScript's signed 32-bit integer)
  • 675 million permission checks per month (average 75 checks per user per day)

With flags columns configurations:

  • JSONB, with the default flags assigned per user.

    Taking space equal to 935 bytes raw -> 809 bytes minified => ~814 bytes (+5 bytes JSONB type)

    {
      "can_view_docs": true,
      "can_edit_docs": false,
      "can_delete_docs": false,
      "can_manage_users": true,
      "can_view_analytics": false,
      "can_export_data": true,
      "can_manage_billing": false,
      "can_create_workspaces": true,
      "can_invite_members": true,
      "can_remove_members": false,
      "can_manage_integrations": false,
      "can_view_audit_logs": true,
      "can_manage_security": false,
      "can_create_projects": true,
      "can_archive_projects": false,
      "can_view_reports": true,
      "can_export_reports": false,
      "can_manage_api_keys": false,
      "can_view_team_activity": true,
      "can_manage_webhooks": false,
      "can_configure_sso": false,
      "can_manage_roles": false,
      "can_view_billing": true,
      "can_change_plan": false,
      "can_access_beta": false,
      "can_manage_domains": false,
      "can_view_usage": true,
      "can_manage_backups": false,
      "can_access_support": true,
      "can_manage_branding": false,
      "can_view_insights": true
    }
    
    Show more
  • 31 boolean columns

    Taking space equal to 31 bytes raw (one column is 1 byte)

  • Integer (bitflags through bitf library, with default flags per user, composed on the application level and sent to the database layer)

    Taking space equal to 4 bytes raw

    import { defineBitflags, bitflag } from 'bitf';
    
    // Define all 31 permission flags
    const Permissions = defineBitflags({
      NONE:                    0,
      CAN_VIEW_DOCS:           1 << 0,   // 0x1
      CAN_EDIT_DOCS:           1 << 1,   // 0x2
      CAN_DELETE_DOCS:         1 << 2,   // 0x4
      CAN_MANAGE_USERS:        1 << 3,   // 0x8
      CAN_VIEW_ANALYTICS:      1 << 4,   // 0x10
      CAN_EXPORT_DATA:         1 << 5,   // 0x20
      CAN_MANAGE_BILLING:      1 << 6,   // 0x40
      CAN_CREATE_WORKSPACES:   1 << 7,   // 0x80
      CAN_INVITE_MEMBERS:      1 << 8,   // 0x100
      CAN_REMOVE_MEMBERS:      1 << 9,   // 0x200
      CAN_MANAGE_INTEGRATIONS: 1 << 10,  // 0x400
      CAN_VIEW_AUDIT_LOGS:     1 << 11,  // 0x800
      CAN_MANAGE_SECURITY:     1 << 12,  // 0x1000
      CAN_CREATE_PROJECTS:     1 << 13,  // 0x2000
      CAN_ARCHIVE_PROJECTS:    1 << 14,  // 0x4000
      CAN_VIEW_REPORTS:        1 << 15,  // 0x8000
      CAN_EXPORT_REPORTS:      1 << 16,  // 0x10000
      CAN_MANAGE_API_KEYS:     1 << 17,  // 0x20000
      CAN_VIEW_TEAM_ACTIVITY:  1 << 18,  // 0x40000
      CAN_MANAGE_WEBHOOKS:     1 << 19,  // 0x80000
      CAN_CONFIGURE_SSO:       1 << 20,  // 0x100000
      CAN_MANAGE_ROLES:        1 << 21,  // 0x200000
      CAN_VIEW_BILLING:        1 << 22,  // 0x400000
      CAN_CHANGE_PLAN:         1 << 23,  // 0x800000
      CAN_ACCESS_BETA:         1 << 24,  // 0x1000000
      CAN_MANAGE_DOMAINS:      1 << 25,  // 0x2000000
      CAN_VIEW_USAGE:          1 << 26,  // 0x4000000
      CAN_MANAGE_BACKUPS:      1 << 27,  // 0x8000000
      CAN_ACCESS_SUPPORT:      1 << 28,  // 0x10000000
      CAN_MANAGE_BRANDING:     1 << 29,  // 0x20000000
      CAN_VIEW_INSIGHTS:       1 << 30,  // 0x40000000
    });
    
    // Compose user permissions matching the JSON example
    const userPermissions = bitflag(Permissions.NONE)
    .add(
      Permissions.CAN_VIEW_DOCS,
      Permissions.CAN_MANAGE_USERS,
      Permissions.CAN_EXPORT_DATA,
      Permissions.CAN_CREATE_WORKSPACES,
      Permissions.CAN_INVITE_MEMBERS,
      Permissions.CAN_VIEW_AUDIT_LOGS,
      Permissions.CAN_CREATE_PROJECTS,
      Permissions.CAN_VIEW_REPORTS,
      Permissions.CAN_VIEW_TEAM_ACTIVITY,
      Permissions.CAN_VIEW_BILLING,
      Permissions.CAN_VIEW_USAGE,
      Permissions.CAN_ACCESS_SUPPORT,
      Permissions.CAN_VIEW_INSIGHTS
    );
    
    userPermissions.value  // Value to store in Integer column in database = 1616904377 (0x60648AB9) (4 bytes)
    
    Show more

PostgreSQL Data volume




PostgreSQL Network Bandwidth

Example responses from the backend to the client handling the communication with database internally:

  • Minimal JSON API Response (with size of 1039 bytes uncompressed and 351 bytes after gzip)

    {
      "userId": 123456,
      "permissions": {
        "can_view_docs": true,
        "can_edit_docs": false,
        "can_delete_docs": false,
        "can_manage_users": true,
        "can_view_analytics": false,
        "can_export_data": true,
        "can_manage_billing": false,
        "can_create_workspaces": true,
        "can_invite_members": true,
        "can_remove_members": false,
        "can_manage_integrations": false,
        "can_view_audit_logs": true,
        "can_manage_security": false,
        "can_create_projects": true,
        "can_archive_projects": false,
        "can_view_reports": true,
        "can_export_reports": false,
        "can_manage_api_keys": false,
        "can_view_team_activity": true,
        "can_manage_webhooks": false,
        "can_configure_sso": false,
        "can_manage_roles": false,
        "can_view_billing": true,
        "can_change_plan": false,
        "can_access_beta": false,
        "can_manage_domains": false,
        "can_view_usage": true,
        "can_manage_backups": false,
        "can_access_support": true,
        "can_manage_branding": false,
        "can_view_insights": true
      }
    }
    
    Show more
  • Miminal JSON API Response withi one JSON field holding bit flags (with size of 42 bytes, non-applicable for gzip)

    {
      "userId": 123456,
      "permissions": 1616904377
    }
    



Arguments against bit flags approach

  • Internal database and application communication systems may not be worth migrating after calculating the risk coming along with the migration and the engineering effort to do it properly. Don't make an issue from things that are not an issue.

  • While introducing the bitflags concept either in cases where eventual optimization gains are significant or in cases where a new codebase is created from scratch - other engineers and developers may say this approach is completely unreadable, e.g. in the code review phase. These concerns are perfectly valid, but please keep in mind that the amount of usually perceived web development "code smells" can be reduced to a minimum in the application code by using the ready-to-copy factory utility function encapsulating the bitflags concept from Implementing bit flags mechanisms in your codebase section or installing the bitf library

Database concerns

  • In most database engines (not only PostgreSQL) bitwise operations are non-SARGable23 (non search-argumentable) meaning they cannot use regular indexes and the conditions to filter huge datasets by some bitwise operation would result in full database sequential scan that may be terrible regarding the performance (>300ms query time).

    There is no need to be alarmed though; in most cases, the results for a bitwise scan can be narrowed from enormous numbers to amounts that just match the criteria.

    For example, having 3 million users checking permissions of a particular organization (let's assume it has 300 users), the results can be narrowed before applying the permission filter

    WHERE organization_id = 123    -- SARGable
    AND created_at > '2024-01-01'  -- SARGable
    AND status = 'active'          -- SARGable
    AND (permissions & 16) = 16;   -- non-SARGable
    

    This will minimize the (possibly parallel) sequential filtering scan of bitwise operations, therefore won't impact performance badly, and the query will take <10ms

Example setup to test performance

The manual experimenting and exploring the performance impact with various setups is highly encouraged.

  1. Setting up the table, seeding the data and creating indexes

        -- Create test table with 3 million rows
      CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        organization_id INTEGER,
        created_at TIMESTAMP,
        status VARCHAR(20),
        permissions INTEGER,
        data JSONB
      );
    
      -- Insert test data
      INSERT INTO users (organization_id, created_at, status, permissions, data)
      SELECT 
        (random() * 1000)::int,
        NOW() - (random() * 365)::int * INTERVAL '1 day',
        CASE WHEN random() > 0.2 THEN 'active' ELSE 'inactive' END,
        (random() * 255)::int,
        jsonb_build_object('name', 'User' || generate_series)
      FROM generate_series(1, 3000000);
    
      -- Create indexes
      CREATE INDEX idx_organization ON users(organization_id);
      CREATE INDEX idx_created_at ON users(created_at);
      CREATE INDEX idx_status ON users(status);
      CREATE INDEX idx_composite ON users(organization_id, status, created_at);
    
    Show more
  2. Query performance measurements

    -- Analyze query without optimization
    EXPLAIN ANALYZE
    SELECT * FROM users
    WHERE (permissions & 16) = 16;
    -- Result: Parallel Sequential Scan, ~300ms for 3M rows
    
    -- Analyze query with SARGable filtering
    EXPLAIN ANALYZE
    SELECT * FROM users
    WHERE organization_id = 123
      AND status = 'active'
      AND created_at > '2024-01-01'
      AND (permissions & 16) = 16;
    -- Result: Index Scan + Filter, ~5ms for same result set
    
    Show more

Runtime data types specifics

While up to 32 booleans may be stored when working with integers in lower-level languages or systems with direct access to the u32 or uint32_t types, the actual recommended number of flags (booleans) to store in JavaScript/TypeScript is 31.

However, there is a good reason for that. Under the hood, JavaScript (and TypeScript) Number runtime type is always a 64-bit IEEE 754 double-precision floating point both as a storage unit and during operations on Numbers. At first glance, it may seem like up to 64 booleans could be stored in one value, but in reality implicit integer operations are 32-bit signed integers. This means that the first bit is a sign bit indicating whether the number is positive or negative and should not be modified manually due to data integrity and compatibility problems. Due to the truncated size during those operations, only 31 bits should actually be considered for usage from the initial 64 bits.

But there's a more worth knowing to that:

  • All Numbers being floating point does not exclude integer bitwise operations. That is why I called these "implicit" in the previous paragraph. When you use the bitwise operators, JavaScript temporarily converts the number from a double-precision floating point to a truncated 32 bit signed integer, performs the operation and then converts it back to a floating point.

    const fakeInteger = 2; // actually stored as 2.0
    const floatingPoint = 8.0; // stored as 8.0
    
    const bitwiseOperationResult = 1 | fakeInteger | 4 | floatingPoint | 16
    //    └─ converted to 1 | 2 | 4 | 8 | 16 == 31 and eventually stored as 31.0
    
  • There are no other ways to work on a different type of numbers in JavaScript apart from bitwise operations or Typed Arrays.

Note about the drawings and sizes presented there

For simplicity purposes I've shortened the numbers on all drawings (figures) since all the zeros would not fit inside the drawings without sacrificing legibility. In real life, 32 bit integers and all operations should be represented in the following way:


Reference drawing explaining the actual count of zeros in binary numbers

I've explored all the numbers using BitwiseCMD4 calculator - exploring it yourself is highly encouraged!

Areas to explore even further

  • When writing this article and creating the bitf library, I also wanted to create separate transpiler plugins for inlining the utility function's usage as babel-plugin-inline-bitf and swc-plugin-inline-bitf, but in the end it would be overkill and a potentially huge area where errors could arise, so eventually I declined this idea.

  • Creating integrations for query builders such as Kysely or integrating it directly in Drizzle or Prisma ORMs can be a clever way to make the bitflags querying between application layer and database layer even easier and more straightforward.

Notes & References

  1. Tagged type from type-fest library: https://github.com/sindresorhus/type-fest/blob/main/source/tagged.d.ts

  2. StackOverflow post about how does database indexing works https://stackoverflow.com/questions/1108/how-does-database-indexing-work

  3. "Sargable" on Wikipedia https://en.wikipedia.org/wiki/Sargable

  4. Brilliant visualizer and calculator for bitwise and hexadecimal arithmetics: https://bitwisecmd.com/

Want to talk about the article? Tag or message me on