DEV Community

Cover image for Bitmask Operations: The Math You Skipped (And Why It Matters)
Abdullah Bashir
Abdullah Bashir

Posted on • Originally published at drreamer.digital

Bitmask Operations: The Math You Skipped (And Why It Matters)

Your computer can check a boolean flag in a single CPU instruction. You're probably doing three memory lookups and a comparison.

If you're here, some linux-torvalds-2.0-like engineer probably said "just use bitmasks" and you nodded along whilst screaming internally. I did that for years. Or maybe you just saw this post in your feed. Either way, lucky you πŸ™ƒ.

This isn't magic. It's just a clever trick with numbers.


How Computers Actually Count

Right, so you know how we count with ten digits (0-9)?

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12...
Enter fullscreen mode Exit fullscreen mode

Computers only have two digits: 0 and 1. Because electricity is either OFF (0) or ON (1).

So they count like this:

0, 1, 10, 11, 100, 101, 110, 111, 1000...
Enter fullscreen mode Exit fullscreen mode

This is called "binary" (base 2) instead of "decimal" (base 10).


What Those 1s and 0s Mean

In our decimal system, each position is worth 10x more than the one to its right:

3 2 5
↑ ↑ ↑
β”‚ β”‚ └─ 5 ones (5 Γ— 1)
β”‚ └─── 2 tens (2 Γ— 10)
└───── 3 hundreds (3 Γ— 100)

325 = 300 + 20 + 5
Enter fullscreen mode Exit fullscreen mode

In binary, each position is worth 2x more:

Position:  3   2   1   0
Value:     8   4   2   1
Enter fullscreen mode Exit fullscreen mode

Let's look at the number 5 in binary: 0101

See? Position 2 is ON (1), worth 4. Position 0 is ON (1), worth 1. That's 4 + 1 = 5.


The Lightbulb Moment

Think of those same four positions as lightbulbs:

[  ] [πŸ’‘] [  ] [πŸ’‘]
 OFF  ON  OFF  ON
 0    1    0    1  = 5
Enter fullscreen mode Exit fullscreen mode

You could describe this as "Bulb 1 is OFF, Bulb 2 is ON, Bulb 3 is OFF, Bulb 4 is ON."

Or you could just write: 5

This is what bitmasks are. Each position (bit) represents a yes/no answer. The entire pattern is stored as a single number.


A Real Example: Feature Flags

You're building an app. Users can toggle features:

  • Dark Mode
  • Analytics
  • Notifications
  • Beta Access

Traditional way:

const userSettings = {
  darkMode: true,
  analytics: false,
  notifications: true,
  betaAccess: false
};
Enter fullscreen mode Exit fullscreen mode

This works. But it takes ~100 bytes of memory (property names, object overhead, etc).

Bitmask way:

Assign each feature a position:

const DARK_MODE      = 1;  // 0001 - Position 0
const ANALYTICS      = 2;  // 0010 - Position 1
const NOTIFICATIONS  = 4;  // 0100 - Position 2
const BETA_ACCESS    = 8;  // 1000 - Position 3
Enter fullscreen mode Exit fullscreen mode

Why those numbers? Because in binary:

  • 1 is 0001 (only position 0 is ON)
  • 2 is 0010 (only position 1 is ON)
  • 4 is 0100 (only position 2 is ON)
  • 8 is 1000 (only position 3 is ON)

A user with Dark Mode and Notifications enabled:

const userSettings = 5; // Binary: 0101
Enter fullscreen mode Exit fullscreen mode

One number. Four settings. 1 byte instead of 100.


The Four Operations

Now let's learn how to actually manipulate these numbers. JavaScript (and most programming languages) has special operators for working with bits - they're called bitwise operators. You've probably seen them before: |, &, ~, ^.

They look weird, like those hieroglyphics plastered everywhere in Assassin's Creed, but each one does something specific to the binary digits. Let me show you.

1. Adding Features (OR: |)

Turn multiple features ON:

let settings = 0; // 0000

settings = DARK_MODE | NOTIFICATIONS;
// Result: 5 (Binary: 0101)
Enter fullscreen mode Exit fullscreen mode

The | operator looks at each position. If EITHER number has a 1, the result has a 1.


2. Checking Features (AND: &)

Test if a feature is enabled:

const settings = 5; // 0101

if (settings & DARK_MODE) {
  console.log('Dark mode enabled');
}
// 0101 & 0001 = 0001 βœ“

if (settings & ANALYTICS) {
  console.log('Analytics enabled');
}
// 0101 & 0010 = 0000 βœ—
Enter fullscreen mode Exit fullscreen mode

The & operator returns 1 only if BOTH positions have a 1. If the result is non-zero, the feature is ON.


3. Removing Features (AND + NOT: & ~)

Turn OFF a specific feature:

let settings = 5; // 0101

settings = settings & ~DARK_MODE;
// Result: 4 (0100)
Enter fullscreen mode Exit fullscreen mode

The ~ operator flips all bits (0β†’1, 1β†’0). Then & keeps everything except the Dark Mode position.


4. Toggling Features (XOR: ^)

Flip a feature (ON→OFF or OFF→ON):

let settings = 5; // Analytics OFF

settings = settings ^ ANALYTICS;
// Analytics now ON

settings = settings ^ ANALYTICS;
// Analytics now OFF
Enter fullscreen mode Exit fullscreen mode

The ^ operator flips the bit. Perfect for toggle switches.


Why This Can Be Better

Memory: When storing many flags, one integer beats an object. My benchmarks show:

  • 10 million users with 4 flags: ~64 MB (bitmask) vs ~1.2 GB (boolean properties)
  • 46% less memory per object

Speed: Bitmask checks are 1.5x faster than array .includes() in benchmarks. Modern compilers optimize both approaches, but avoiding property lookups and reducing memory bandwidth can matter at scale - especially at thousands of operations per second.

Atomic: A single integer assignment is atomic. Multiple property updates aren't.

Important context: The performance difference depends on your architecture, compiler, and usage patterns. Always measure in your specific case before optimizing.

Full benchmarks and code


Real Problems This Solves

Network Protocols

TCP sends flags (SYN, ACK, FIN, RST) with every packet. Instead of { syn: true, ack: false, ... } (dozens of bytes), it sends one byte: 10010001.

When you're sending billions of packets, every byte counts.


Permission Systems

Checking user.perms & CAN_DELETE is faster than querying a database or doing multiple property lookups, especially at thousands of requests per second.

We'll build this properly in Part 2.


The Limitations

1. Limited by integer size

JavaScript bitwise ops: 32 bits max. Need more? Use multiple numbers or BigInt.

2. Less readable

if (flags & 0x04)           // ???
if (settings.notifications) // Clear
Enter fullscreen mode Exit fullscreen mode

Always use named constants.

3. Can't add flags dynamically

settings[userDefinedFlag] = true;  // Works with objects
settings & userDefinedFlag;        // Doesn't work
Enter fullscreen mode Exit fullscreen mode

Bitmasks need pre-defined positions.

4. Debugging is harder

console.log(settings); // 23
// What does 23 mean?
Enter fullscreen mode Exit fullscreen mode

Write helper functions to decode during development.


When to Actually Use This

Use bitmasks when:

  • Checking flags in hot code paths (thousands of times/sec)
  • 5+ related yes/no values
  • Memory genuinely matters (millions of records, embedded systems)
  • You need atomic flag updates

Don't use bitmasks when:

  • 2-3 flags total (overhead isn't worth it)
  • Readability matters more than performance
  • Flags are dynamic/user-defined
  • You're not hitting actual performance bottlenecks

Remember: Modern compilers are smart. Measure before optimizing.

PS: I know these operators feel unnatural at first. So did || for default values, arrow functions, and destructuring. Once you're over the learning curve, they become second nature - unless your team's style guide bans them, in which case... well, that's a different conversation. πŸ˜…


What's Next

You now understand:

  • Binary (powers of 2)
  • Packing multiple yes/no values into one number
  • The four operations: OR, AND, AND+NOT, XOR
  • When it helps (and when it doesn't)

In Part 2, I'll show you how to build role-based permissions using bitmasks. We'll check user permissions with bitwise ops instead of database queries.

You'll see why Unix uses chmod 755, why game engines store player state this way, and when your auth system should too.

Happy Hacking!


Originally published on https://drreamer.digital/blog/bitmask-operations-the-math-you-skipped

Top comments (0)