July 27, 2025

Fixing Heavy JWTs: Bitmasking for Lean & Scalable Permissions

Posted on July 27, 2025  •  7 minutes  • 1361 words

Ever hit a wall with an error message that just screams, “Dude, you’re sending too much data!” Or maybe your shiny, new Single Page Application (SPA) suddenly starts acting weird, requests dropping like flies? Yeah, I’ve been there. For us, the culprit turned out to be our good old friend, the JSON Web Token (JWT), specifically when it got absolutely packed with permissions and claims. What started as a neat, efficient way to handle authorization quickly spiraled into a monster that was literally too big for its britches.

The Problem: When JWTs Get Greedy

Here’s what I’ve been through: I started with basic JWT claims, just iss, exp, iat, userid, and email. It typically looks something like this (decoded for clarity):

// Example of a basic, decoded JWT payload
{
  "iss": "your-auth-server.com",
  "exp": 1735689600,
  "iat": 1704067200,
  "userid": "user123",
  "email": "user@example.com"
}

Then came the permissions. Initially, a few dozen strings for services like USER_CREATE, PRODUCT_VIEW, etc., were fine. But then permissions got really granular. Think INVENTORY_ITEM_CREATE_BATCH, SALES_REPORT_VIEW_BY_REGION. Suddenly, that permissions array wasn’t just a few dozen strings; it was hundreds, sometimes thousands.

Here’s a glimpse of what an overstuffed JWT payload looked like:

// Example of an overstuffed JWT payload
{
  "iss": "your-auth-server.com",
  "exp": 1735689600,
  "iat": 1704067200,
  "userid": "user_with_all_the_permissions",
  "email": "user@example.com",
  "permissions": [
    "USER_CREATE", "USER_READ", "PRODUCT_VIEW", "ORDER_CREATE",
    "INVENTORY_ADD", "SALES_VIEW_ALL", "REPORT_GENERATE_FINANCIAL",
    // ... many, many more. This list alone could easily push the JWT well past 4KB when encoded.
  ]
}

Let’s Talk Numbers: Calculating JWT Size

So, how does a few dozen permissions turn into a monster token? It’s simple math, really, plus a bit of JSON and Base64 encoding overhead. The size issue almost always comes down to the Payload.

Let’s break down the approximate size contributors:

ComponentEstimated JSON Characters/BytesNotes
JWT Header (JSON String)~40 charactersExample: {"alg":"HS256","typ":"JWT"}
Fixed Payload Claims (JSON String)~200 charactersiss, exp, iat, userid, email, plus the permissions array structure {"permissions":[]}
Average Permission String Length10 charactersE.g., USER_CREATE
Overhead per Permission in Array~3 charactersFor quotes ("") and comma (,) in JSON, i.e., "PERMISSION",
Effective Length per Permission in JSON Payload13 characters(10 characters for string + 3 characters for overhead)
Base64Url Encoding Overhead~33%Converts 3 bytes of raw data into 4 Base64 characters
JWT Signature (Encoded)~43 bytesFixed size for HS256 algorithm

To hit those common HTTP header limits, like 4KB or 8KB, it doesn’t take as many permissions as you’d think. Based on these numbers, having around 200-220 permissions, each just 10 characters long, can easily push your encoded JWT to over 4.5 KB. This immediately puts you at risk of those dreaded “Request Header Fields Too Large” errors from your web server or proxy.


Solution: Bitmasking to the Rescue!

Okay, so we’ve got this heavy JWT problem. What’s the fix? Well, I gotta tell you, I was doing some digging, and I stumbled across how Linux handles permissions using bitmasking—it’s both efficient and incredibly scalable. Lightbulb moment! I figured, “Hey, if it works for Linux, why can’t we use this magic for our JWT permissions, especially now that they’re multiplying like rabbits?“0

🔐 From Linux Permissions to Our Lean, Mean JWT System

In Linux, file permissions are a classic example of bitmasking in action. Each permission—read (r), write (w), and execute (x)—is represented by a specific bit. Combine them, and boom you’ve got a permission mask.

RoleBinaryOctalSymbolic
Owner1117rwx
Group1015r-x
Others1015r-x

Inspired by this efficiency, I cooked up a JWT-based permission model using bitmasking in PHP. Instead of slapping in a bunch of separate flags, we assign each permission a unique bit, like 1 << n.

Here’s how we set up our permission flags:

const PERMISSION_FLAGS = [
    "BLOG_CREATE" => 1 << 0,  // 1
    "BLOG_READ"   => 1 << 1,  // 2
    "BLOG_UPDATE" => 1 << 2,  // 4
    // ...
    "PERMISSION_64" => 1 << 63 // A whopping 2^63!
];

🔍 Why this works so darn well:

✅ Example: Checking and Calculating Permissions It’s Easy Peasy!

Want to see how concise checking and calculating permissions becomes? Here’s the deal for the backend (PHP example):

<?php
const PERMISSION_FLAGS = [
    "BLOG_CREATE" => 1 << 0, "BLOG_READ" => 1 << 1, "BLOG_UPDATE" => 1 << 2,
    "BLOG_DELETE" => 1 << 3, "COMMENT_CREATE" => 1 << 4, "DASHBOARD_ACCESS" => 1 << 5,
    "PERMISSION_33" => 1 << 32
];

function calculatePermissionMask(array $permissionNames): int {
    $mask = 0;
    foreach ($permissionNames as $name) {
        if (isset(PERMISSION_FLAGS[$name])) { $mask |= PERMISSION_FLAGS[$name]; }
    }
    return $mask;
}

function hasPermission(int $userPermissionMask, string $permissionToCheck): bool {
    if (!isset(PERMISSION_FLAGS[$permissionToCheck])) { return false; }
    return ($userPermissionMask & PERMISSION_FLAGS[$permissionToCheck]) !== 0;
}

$userPermissions = calculatePermissionMask(["BLOG_READ", "COMMENT_CREATE", "DASHBOARD_ACCESS", "PERMISSION_33"]);
echo "User mask (decimal): " . $userPermissions . "\n";
echo "Can read blog? " . (hasPermission($userPermissions, "BLOG_READ") ? "Yes" : "No") . "\n";
echo "Can delete blog? " . (hasPermission($userPermissions, "BLOG_DELETE") ? "Yes" : "No") . "\n";
?>

And for the Frontend (Vanilla JavaScript)

Since our frontend uses these JWTs to decide what to show and hide, we need a similar setup there. Good news: bitwise operations are universal!

const PERMISSION_FLAGS_JS = {
    "BLOG_CREATE": 1 << 0, "BLOG_READ": 1 << 1, "BLOG_UPDATE": 1 << 2,
    "BLOG_DELETE": 1 << 3, "COMMENT_CREATE": 1 << 4, "DASHBOARD_ACCESS": 1 << 5,
    "PERMISSION_33": 1 << 32, "PERMISSION_64": 1 << 63
};

// Assume 'permissionMask' comes from your decoded JWT payload
const decodedJwtPayload = { "permissionMask": 8589934642 };
const userPermissionMaskJS = decodedJwtPayload.permissionMask;

function hasPermissionJS(permissionToCheck) {
    if (!(permissionToCheck in PERMISSION_FLAGS_JS)) { return false; }
    return (userPermissionMaskJS & PERMISSION_FLAGS_JS[permissionToCheck]) !== 0;
}

console.log("Can view blog module? ", hasPermissionJS("BLOG_READ") ? "Yes!" : "No.");
// Dynamically showing/hiding a button
const createBlogButton = document.getElementById('createBlogButton');
if (createBlogButton) {
    createBlogButton.style.display = hasPermissionJS("BLOG_CREATE") ? 'block' : 'none';
}
console.log("Can access settings? ", hasPermissionJS("SETTINGS_SYSTEM_CONFIG") ? "Yep!" : "Nah.");

This logic echoes Linux’s philosophy: use bits for what they’re best at—fast and compact access control.

🚀 Final Thought: Bitmasking - Not Just for Old-School Geeks!

Bitmasking isn’t some dusty relic from system programming textbooks it’s a timeless solution, pure and simple. It lets you express a ton of complexity with surprising simplicity.

And guess what? It directly tackles our heavy JWT problem head-on. Remember that calculation where just 200-220 granular permissions could swell your JWT up to an estimated 4.5KB or more, risking those annoying server errors?

With bitmasking, all those hundreds of permissions collapse into a highly compact numerical representation (often just one number, or a few if you’re really piling on the permissions). The difference is night and day. Take a look at the drastic improvement in size:

FeatureBefore (String Array for 212 Permissions)After (Bitmask for 212+ Permissions)
Permission RepresentationAn array of strings: ["USER_CREATE", "USER_READ", ...]A compact numerical value (or values): e.g., 9223372036854775807
Payload ContentThousands of characters for permissions array contentAround 20-80 characters (for 1-4 integers) for numerical mask(s)
Approximate Encoded JWT Size~4.5 KB (4500 bytes)~0.33 KB - ~0.6 KB (330-600 bytes)
HTTP Header CompatibilityOften exceeds common 4KB/8KB limits, causing errorsEasily stays well under 1KB, no issues

Your JWT goes from being a heavyweight contender to a featherweight champion! It’s elegant, efficient, and as we’ve literally just seen, a fantastic way to keep your JWTs lean, mean, and actually working instead of throwing TooLargeRequest tantrums. Give it a try!

Follow me

...