The signup worked in every check I had run, and the first real one failed. The password hash was set to 150,000 PBKDF2 iterations, and Cloudflare Workers caps that at 100,000.
What I was building
We had a client portal prototype where any password worked. 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 I picked 150,000. It sat comfortably above the 100,000 figure I remembered from older guidance, and I did not look up what Workers allows.
The wrong turn: choosing the number from memory
Nothing I had could have 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.
The fix, and the verification that mattered
The fix was one number, 100,000, with a comment beside it saying why it cannot be higher. Then I ran the whole path against the live service instead of trusting the change:
- A new email gets the create-your-password screen, and creating the account lands on onboarding.
- Signing back in with the wrong password shows the inline error, with no session and no dashboard.
- The right password goes straight to the dashboard.
- 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 I would tell my earlier self
A platform limit is a fact about the platform, and I had stored it in my head from a different context. The check took a minute: look up the limit for the exact call before choosing the parameter.
The bigger habit is what I 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.