---
title: "Packing Many On/Off Switches Into One Number: Bitmasks for Permissions and IPs"
url: https://phatdev.net/en/blog/bitwise-in-practice
lang: en
updated: 2026-10-21
alternate:
  vi: https://phatdev.net/vi/blog/bitwise-in-practice
markdown: https://phatdev.net/en/blog/bitwise-in-practice.md
---

# Packing Many On/Off Switches Into One Number: Bitmasks for Permissions and IPs

> Store a dozen user permissions in one numeric column, check whether an IP address is in an allowed range, and avoid the traps JavaScript sets when you work with bits.

Categories: Backend | Published: 2026-10-19 | Level: Intermediate | 7 min read

Tags: bitwise, bitmask, permissions, subnet, cidr, javascript, bigint, python

Your users table has columns `can_read`, `can_write`, `can_delete`, `is_admin`. Next week someone asks for an "export" permission, so you add a fifth column, change the API, change the screens, and hope you missed nothing. There is a way to store all of those permissions in a single number, with no table change when a new one shows up.

It is called a bitmask. It sounds like something only driver writers need, but I run into it all the time in web work: storing permissions, turning features on and off, checking whether an IP address falls inside an allowed range.

![A row of toggle switches, some lit, with an amber mask frame above them](https://phatdev.net/uploads/blog/bitwise-in-practice/bit-flags-1920.webp)

## One number, many switches

A computer stores numbers as 0s and 1s, and each of those digits is called a bit. Think of each bit as a light switch: 1 is on, 0 is off.

Give each permission its own switch: read is switch 0, write is switch 1, delete is switch 2, admin is switch 3. The number `5` written in bits is `0101`, which means the read and delete switches are on. One number, four permissions.

You have met this before without noticing. `chmod 755` on Linux is a bitmask: read is 4, write is 2, execute is 1, and `7 = 4 + 2 + 1` means all three.

## Four moves cover daily use

Call the number that holds the switches `mask`, and the switch you care about `flag`. You need four moves:

- Turn a permission on: `mask | flag`
- Turn it off: `mask & ~flag`
- Flip it: `mask ^ flag`
- Ask "is it on?": `(mask & flag) === flag`

You do not have to understand every symbol right away. Just remember that `&` works like a mask frame laid over the number: you only see the switches that sit inside the frame.

## How to do it

1. List your permissions and give each one a power of 2: 1, 2, 4, 8, 16...
2. Name each number in code (`READ`, `WRITE`). Never write bare numbers.
3. Store the whole number in one integer column in the database.
4. Route every permission check through shared `has`, `grant` and `revoke` helpers, so the bit math does not get copied around.
5. To add a permission, only add it at the end (the next number in the sequence) and never change the number of an existing one. If you do, old data in the database starts meaning something else.

One easy slip: "has both permissions" is different from "has at least one of them". Mixing them up grants more than you intended.

## For readers who write code

I reran everything on Node 20, Python 3.12 and MariaDB 11.8.

JavaScript:

```js
export const Perm = Object.freeze({
  READ:   1 << 0, // 0001
  WRITE:  1 << 1, // 0010
  DELETE: 1 << 2, // 0100
  ADMIN:  1 << 3, // 1000
});

export const has    = (mask, flag) => (mask & flag) === flag;
export const grant  = (mask, flag) => mask | flag;
export const revoke = (mask, flag) => mask & ~flag;
export const toggle = (mask, flag) => mask ^ flag;

let user = Perm.READ | Perm.WRITE;   // 0011
user = grant(user, Perm.DELETE);     // 0111
user = revoke(user, Perm.WRITE);     // 0101
user = toggle(user, Perm.ADMIN);     // 1101
```

The steps come out as `0011`, `0111`, `0101`, `1101`. For a user at `0101`, `has(user, READ | WRITE)` gives `false` because write is missing, while `(user & (READ | WRITE)) !== 0` gives `true` because at least one is present. They ask different questions, so pick the one you mean.

The most dangerous trap is a missing bracket. In JavaScript, `===` is evaluated before `&`. Writing `user & Perm.WRITE === Perm.WRITE` is read as `user & (Perm.WRITE === Perm.WRITE)`, which is `user & true`, which is `user & 1`. I tried it on a user whose write permission had been removed: `has` correctly said `false`, while the unbracketed version said "yes" because the read switch was on. That is a real authorization bug. Always wrap `(mask & flag)` in brackets.

Python ships `IntFlag` for exactly this: `Perm.WRITE in user` to ask, `user |= Perm.DELETE` to grant, `user &= ~Perm.WRITE` to revoke, and `Perm(5)` rebuilds the set from the number stored in the database (it prints `<Perm.READ|DELETE: 5>`).

In the database a single `perms INT UNSIGNED` column is enough. I tried this on a temporary database with four users:

```sql
SELECT name FROM users WHERE perms & 2 = 2;                -- who can write
UPDATE users SET perms = perms | 8 WHERE name = 'dung';    -- grant admin
UPDATE users SET perms = perms & ~2 WHERE name = 'an';     -- revoke write
```

In MySQL and MariaDB, `&` is evaluated before `=`, so the first query does what you expect (unlike JavaScript). The cost: `perms & 2 = 2` is a calculation on the column, so the database cannot use a normal index, and `EXPLAIN` shows `type: ALL`, meaning it reads the whole table. That is fine for a few thousand users. For millions of rows you need to filter quickly, use a separate indexed column. The reasons are in [Slow Query? How to Read EXPLAIN and Put Indexes in the Right Place](https://phatdev.net/en/blog/mysql-index-explain).

## Using bits to check an IP address

An IPv4 address is really one 32-bit number. When you see `192.168.10.77/24`, the 24 means the first 24 bits name the "neighborhood" and the last 8 name the "house in that neighborhood". To test whether an IP belongs to the neighborhood, lay a mask of 24 ones over it and compare with the neighborhood address.

![A grid of 32 tiles, 24 teal tiles for the network part and 8 violet tiles for the host part](https://phatdev.net/uploads/blog/bitwise-in-practice/subnet-mask-1920.webp)

```js
export function ipToInt(ip) {
  const p = ip.split(".").map(Number);
  if (p.length !== 4 || p.some((x) => !Number.isInteger(x) || x < 0 || x > 255)) throw new Error("IPv4 không hợp lệ");
  return ((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]) >>> 0;
}

export function prefixToMask(prefix) {
  if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error("prefix 0..32");
  return prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0;
}

export function inCidr(ip, cidr) {
  const [base, p] = cidr.split("/");
  const mask = prefixToMask(Number(p));
  return ((ipToInt(ip) & mask) >>> 0) === ((ipToInt(base) & mask) >>> 0);
}
```

Running it on `192.168.10.77/24` gives network `192.168.10.0`, mask `255.255.255.0`, 254 machines. On `10.0.0.5/13` it gives mask `255.248.0.0`. I also compared this against Python's `ipaddress` module on 2,000 random cases and none differed.

Two things in this snippet must stay. One is `>>> 0`: JavaScript returns signed 32-bit numbers, so without it `200.1.1.1` AND `255.0.0.0` comes out as the negative `-939524096` and the comparison goes wrong. The other is the `prefix === 0` branch: JavaScript takes the shift count modulo 32, so `x << 32` becomes `x << 0` and `/0` yields a mask of all ones instead of all zeros. For IPv6 or anything important, use a ready-made library (`ipaddress` in Python, `net.BlockList` in Node).

## Why JavaScript keeps surprising people

JavaScript forces every bit operation into a signed 32-bit integer. Cases I ran:

- `1 << 31` gives `-2147483648`, and `1 << 32` gives `1`, not 4294967296.
- `2 ** 32 | 0` gives `0`. Anything past 32 bits is silently cut, so a millisecond timestamp passed through `| 0` is ruined.
- `~5` gives `-6`, not `10`.

Need more than 32 bits? Use `BigInt`: `1n << 40n` gives `1099511627776n`. You cannot mix BigInt with ordinary numbers, and `1n | 1` throws a `TypeError`. Python avoids these problems because its integers have no size limit, though `~5` is also `-6` there, and you add `& 0xFF` yourself if you want a fixed width.

## Mistakes I keep seeing

The most painful one is inserting a new permission in the middle of the sequence and renumbering for tidiness. The stored data keeps its old numbers, only the meaning of the numbers changes, so every existing user quietly gets different permissions. That is why the rule says to add only at the end.

Another classic is using a number that is not a power of 2, such as `3`, for a permission. `3` is two switches on at once, so that permission sticks to another one.

And in the IP problem, someone adds `| 0` or `>> 0` "to keep it short" and then wonders why large addresses compare wrongly. The JavaScript section above has the answer.

## What to do next

If your table has a pile of true/false columns for permissions or features, try folding them into one bitmask column for small data, and keep `has`, `grant` and `revoke` in a single place. If a permission needs fast filtering on a big table, keep a separate indexed column. The two approaches do not exclude each other; choose by where you need them.

## References

- [MDN: Bitwise AND and other bitwise operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_AND)
- [MDN: Operator precedence](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence)
- [MDN: BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)
- [Python docs: enum.IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag)
- [Python docs: ipaddress](https://docs.python.org/3/library/ipaddress.html)
- [Node.js docs: net.BlockList](https://nodejs.org/api/net.html#class-netblocklist)
- [MariaDB: Operator precedence](https://mariadb.com/kb/en/operator-precedence/)

## Related posts

- [Slow Query? How to Read EXPLAIN and Put Indexes in the Right Place](https://phatdev.net/en/blog/mysql-index-explain.md)
- [Getting Started with Python in 2026: Install, a Clean Setup and Your First Script](https://phatdev.net/en/blog/python-start-2026.md)
- [Automating Office Work with Python: Merge Excel Files, Build a Report and Send the Email](https://phatdev.net/en/blog/python-office-automation.md)
