Many applications need a long list of on/off settings per user, per account, or per record: email notifications on, dark mode off, beta features enabled. You can store each one in its own column or field, or you can pack them into the bits of one integer. This guide covers when packing makes sense and how to do it without creating a maintenance problem.
The Idea
Assign each boolean a bit position. The integer’s value is the OR of all enabled flags:
| Bit | Flag | Value |
|---|---|---|
| 0 | EMAIL_NOTIFICATIONS | 1 |
| 1 | SMS_NOTIFICATIONS | 2 |
| 2 | DARK_MODE | 4 |
| 3 | BETA_FEATURES | 8 |
| 4 | TWO_FACTOR | 16 |
| 5 | NEWSLETTER | 32 |
A user with email notifications, dark mode, and two-factor auth enabled has the value 1 | 4 | 16 = 21, or 00010101 in binary. Eight flags fit in one byte, 32 in a 32-bit integer, and 64 in a 64-bit integer.
Why Use Bitfields?
- Compact storage. One integer column instead of dozens of boolean columns, which matters in very large tables, network packets, and embedded memory.
- Atomic updates. Changing several flags at once is a single write.
- Fast set operations. “Does the user have all of these flags?” is one AND and one compare.
- Easy transport. One number passes cleanly through APIs, URLs, and caches.
Why Not?
- Readability.
flags = 21means nothing without the table above. Raw numbers in logs and databases are hard to read. - Limited size. You run out of bits eventually, and widening the column is a migration.
- Harder queries and indexes. Most databases can’t index individual bits efficiently.
- Bit positions are forever. Once a bit is written to storage, its meaning can’t change.
A good rule of thumb: use bitfields for small, stable sets of flags that are usually read together. Use separate columns or a key-value table for flags that are queried individually or change often.
Implementation in C
#include <stdint.h>
#include <stdbool.h>
enum {
FLAG_EMAIL = 1u << 0,
FLAG_SMS = 1u << 1,
FLAG_DARK_MODE = 1u << 2,
FLAG_BETA = 1u << 3,
FLAG_TWO_FACTOR = 1u << 4,
FLAG_NEWSLETTER = 1u << 5,
};
static inline bool has(uint32_t f, uint32_t m) { return (f & m) == m; }
static inline uint32_t enable(uint32_t f, uint32_t m) { return f | m; }
static inline uint32_t disable(uint32_t f, uint32_t m) { return f & ~m; }
Writing helpers like has() instead of inline & expressions removes the operator-precedence trap (f & m == m is wrong in C) and makes intent obvious.
Implementation in Python and JavaScript
Python’s enum.Flag gives you named bits with readable output:
from enum import IntFlag, auto
class Feature(IntFlag):
EMAIL = auto() # 1
SMS = auto() # 2
DARK_MODE = auto() # 4
BETA = auto() # 8
TWO_FACTOR = auto() # 16
f = Feature.EMAIL | Feature.DARK_MODE | Feature.TWO_FACTOR
print(f, int(f)) # Feature.EMAIL|DARK_MODE|TWO_FACTOR 21
print(Feature.BETA in f) # False
In JavaScript, a frozen object works well. Stay below bit 31, or use >>> 0 when reading, because bitwise results are signed 32-bit integers (see JavaScript Bitwise Operators):
const Feature = Object.freeze({
EMAIL: 1 << 0, SMS: 1 << 1, DARK_MODE: 1 << 2, BETA: 1 << 3, TWO_FACTOR: 1 << 4,
});
const has = (flags, mask) => (flags & mask) === mask;
let flags = Feature.EMAIL | Feature.DARK_MODE;
flags |= Feature.TWO_FACTOR;
has(flags, Feature.DARK_MODE); // true
Querying Bitfields in SQL
Most SQL databases support bitwise operators:
-- Users with dark mode (bit 2) enabled
SELECT id FROM users WHERE (flags & 4) <> 0;
-- Users with BOTH email (1) and two-factor (16)
SELECT id FROM users WHERE (flags & 17) = 17;
-- Turn on beta (8) for one user
UPDATE users SET flags = flags | 8 WHERE id = 42;
-- Turn off SMS (2) for everyone
UPDATE users SET flags = flags & ~2;
These queries usually can’t use a normal index, so they scan the table. If you often filter on one flag in a large table, give that flag its own indexed column.
Design Rules That Prevent Bugs
- Never reuse a retired bit. If
BETAis removed, leave bit 3 unused. Old rows still have it set, and a new flag in that position would inherit random values. - Write the table down. Keep the bit assignments in one source file, documented, and generate other languages from it if needed.
- Use unsigned types in C and C++, so bit 31 is just another bit.
- Reserve room. If you have 10 flags today, use a 32-bit column, not a 16-bit one.
- Print in binary while debugging.
00010101is far easier to read than21. The Binary Bitwise Calculator shows each bit position directly.
Where You’ve Already Seen This
Bitfields are everywhere once you look: Unix file permissions (what chmod 755 means), the flags passed to open() and mmap(), TCP header flags (SYN, ACK, FIN), Windows window styles, Discord and other chat-app permission integers, and CPU status registers. Each is a set of named bits combined with OR and tested with AND. The underlying techniques are covered step by step in the Bit Masking Guide.