Everything About Bitflags
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:
- If the default browser's spell checker is being disabled (name of the flag:
BUILTIN_SPELLCHECK_DISABLED
) - If the third-party spell checkers is being disabled (name of the flag:
THIRD_PARTY_SPELLCHECK_DISABLED
) - 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
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".
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.
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
-
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 );
-
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
- Manually just push the integer after bitwise operations on the application level
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
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.
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
- 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
- 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
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:
- Permission systems and roles (authorization).
- Comprehensive and heavy logging/telemetry/analytics mechanisms.
- 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
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)
-
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
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 and351 bytes
after gzip) -
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
Strategic & DX related drawbacks
-
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.
-
Setting up the table, seeding the data and creating indexes
-
Query performance measurements
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 Number
s. 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
Number
s 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:
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 asbabel-plugin-inline-bitf
andswc-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
-
Tagged
type fromtype-fest
library: https://github.com/sindresorhus/type-fest/blob/main/source/tagged.d.ts -
StackOverflow post about how does database indexing works https://stackoverflow.com/questions/1108/how-does-database-indexing-work
-
"Sargable" on Wikipedia https://en.wikipedia.org/wiki/Sargable
-
Brilliant visualizer and calculator for bitwise and hexadecimal arithmetics: https://bitwisecmd.com/