---
title: "We Picked 150,000 Password Hash Iterations and the Platform Said No"
canonical: https://dxdev.com/blog/cf-pbkdf2-workers-limit/
datePublished: 2026-07-09
---
The [signup worked in every check we had run](/everyday-ai/every-check-passed-the-first-customer-broke-it/), and the first real one failed. The password hash was set to 150,000 PBKDF2 iterations, and Cloudflare Workers caps that at 100,000.

## A prototype where any password worked

We had a client portal prototype where any password was accepted. The login screen even said so. It ran on a Worker with a Durable Object holding the accounts, and the job was to turn it into a real product: real password creation, real rejection of wrong passwords, and service requests that persist instead of a fake checkout.

The design was ordinary. PBKDF2 with SHA-256 through WebCrypto, a random salt per user, the salt and iteration count stored next to the hash, and a constant-time comparison on login. A new account hashes and stores the password and starts a session. An existing account with the wrong password gets a 401 and no session.

For the iteration count we picked 150,000. It sat comfortably above the 100,000 figure we remembered from older guidance, and we did not look up what Workers allows. That was the wrong turn, and we took it from memory.

## Why nothing caught it

The iteration count is a runtime argument to one function. A build does not execute that function, and neither does a type check, so both were clean. The first call that reaches the hashing step with real parameters is a real signup on the deployed Worker, and that call returned a server error.

The symptom was narrow. The check endpoint answered correctly and the client side behaved. Only the step that creates the hash failed, and only in the deployed runtime. That pointed straight at the one line that depended on the platform.

The platform rejects PBKDF2 above 100,000 iterations. It does not clamp the value. It throws.

## One number, then the whole path

The fix was one number, 100,000, with a comment beside it saying why it cannot be higher. Then we ran the whole path against the live service instead of trusting the change:

1. A new email gets the create-your-password screen, and creating the account lands on onboarding.
2. Signing back in with the wrong password shows the inline error, with no session and no dashboard.
3. The right password goes straight to the dashboard.
4. A service request is recorded and shows as requested after a reload.

All four passed. That night the change was committed with the real login, the recorded requests, and every prototype badge removed.

## What the miss cost us

A platform limit is a fact about the platform, and we had stored it from a different context. Looking up the limit for the exact call would have taken a minute.

It also changed what we count as a test. A signup that never reaches the hashing step proves the form works. The proof that the login is real is the one that creates a real account on the real runtime, then tries the wrong password, then the right one.

The number is 100,000 now, and the comment next to it is the first thing anyone changing it will read.
