[GH-ISSUE #8860] bug: email/password sign-up exceeds CPU time limit on Cloudflare Workers #19848

Closed
opened 2026-04-15 19:12:01 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @chrisdadev13 on GitHub (Mar 31, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8860

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

Reproduction

  1. Deploy a better-auth app to Cloudflare Workers with emailAndPassword enabled
  2. POST /api/auth/sign-up/email with name, email, and password
  3. Request intermittently fails with "Worker exceeded CPU time limit"

Current vs. Expected behavior

Sign-up fails intermittently with Worker exceeded CPU time limit. The pure JS scrypt from @noble/hashes is right on the edge of Workers' CPU budget, sometimes it finishes, sometimes it doesn't.

Expected: sign-up should complete reliably.

Workaround: providing a custom emailAndPassword.password.hash/verify using native node:crypto.scryptSync with the same params resolves it. #8685 would fix this upstream.

What version of Better Auth are you using?

1.4.18

System info

Runtime: Cloudflare Workers (stateless execution model)
nodejs_compat enabled
Alchemy (IaC) with compatibility: "node"

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

export const auth = betterAuth({
    emailAndPassword: {
      enabled: true,
      requireEmailVerification: true,
    },
    advanced: {
      disableCSRFCheck: true,
    },
    // ...plugins, db adapter, etc.
});

Additional context

import crypto from "node:crypto";

const SCRYPT_PARAMS = { N: 16384, r: 16, p: 1, maxmem: 128 * 16384 * 16 * 2 };

async function hashPassword(password: string): Promise<string> {
	const salt = crypto.randomBytes(16);
	const key = crypto.scryptSync(password.normalize("NFKC"), salt, 64, SCRYPT_PARAMS);
	return `${salt.toString("hex")}:${key.toString("hex")}`;
}

async function verifyPassword({
	hash,
	password,
}: {
	hash: string;
	password: string;
}): Promise<boolean> {
	const [saltHex, keyHex] = hash.split(":");
	if (!saltHex || !keyHex) return false;
	const key = crypto.scryptSync(
		password.normalize("NFKC"),
		Buffer.from(saltHex, "hex"),
		64,
		SCRYPT_PARAMS,
	);
	return crypto.timingSafeEqual(key, Buffer.from(keyHex, "hex"));
}

export const auth = betterAuth({
	database: drizzleAdapter(db, {
		provider: "pg",
		schema: schema,
	}),
	trustedOrigins: [env.CORS_ORIGIN],
	emailAndPassword: {
		enabled: true,
		requireEmailVerification: true,
		password: {
			hash: hashPassword,
			verify: verifyPassword,
		},
	},
});

No response

Originally created by @chrisdadev13 on GitHub (Mar 31, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8860 Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### Reproduction 1. Deploy a better-auth app to Cloudflare Workers with `emailAndPassword` enabled 2. POST `/api/auth/sign-up/email` with name, email, and password 3. Request intermittently fails with "Worker exceeded CPU time limit" ### Current vs. Expected behavior Sign-up fails intermittently with Worker exceeded CPU time limit. The pure JS scrypt from `@noble/hashes` is right on the edge of Workers' CPU budget, sometimes it finishes, sometimes it doesn't. Expected: sign-up should complete reliably. **Workaround**: providing a custom `emailAndPassword.password.hash/verify` using native `node:crypto.scryptSync` with the same params resolves it. #8685 would fix this upstream. ### What version of Better Auth are you using? 1.4.18 ### System info ```bash Runtime: Cloudflare Workers (stateless execution model) nodejs_compat enabled Alchemy (IaC) with compatibility: "node" ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: true, }, advanced: { disableCSRFCheck: true, }, // ...plugins, db adapter, etc. }); ``` ### Additional context ```typescript import crypto from "node:crypto"; const SCRYPT_PARAMS = { N: 16384, r: 16, p: 1, maxmem: 128 * 16384 * 16 * 2 }; async function hashPassword(password: string): Promise<string> { const salt = crypto.randomBytes(16); const key = crypto.scryptSync(password.normalize("NFKC"), salt, 64, SCRYPT_PARAMS); return `${salt.toString("hex")}:${key.toString("hex")}`; } async function verifyPassword({ hash, password, }: { hash: string; password: string; }): Promise<boolean> { const [saltHex, keyHex] = hash.split(":"); if (!saltHex || !keyHex) return false; const key = crypto.scryptSync( password.normalize("NFKC"), Buffer.from(saltHex, "hex"), 64, SCRYPT_PARAMS, ); return crypto.timingSafeEqual(key, Buffer.from(keyHex, "hex")); } export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema: schema, }), trustedOrigins: [env.CORS_ORIGIN], emailAndPassword: { enabled: true, requireEmailVerification: true, password: { hash: hashPassword, verify: verifyPassword, }, }, }); ``` _No response_
GiteaMirror added the bug label 2026-04-15 19:12:01 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 31, 2026):

Hi @chrisdadev13! I'm Dosu and I’m helping the better-auth team.

This is a known problem — the pure-JS scrypt from @noble/hashes with N=16384, r=16, p=1 consistently takes ~4.5–5s of CPU time on Cloudflare Workers, which is right at or over their CPU budget [1].

PR #8685 (perf(core): use non-blocking scrypt from @better-auth/utils) is currently open and awaiting review — it directly addresses this issue [2].

In the meantime, the workaround you mentioned works well. For anyone else hitting this, you can override the hash/verify functions in your config [3]:

import { scryptSync, randomBytes, timingSafeEqual } from "node:crypto";

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    password: {
      hash: async (password) => {
        const salt = randomBytes(16).toString("hex");
        const key = scryptSync(password, salt, 64, { N: 16384, r: 16, p: 1 });
        return `${salt}:${key.toString("hex")}`;
      },
      verify: async ({ hash, password }) => {
        const [salt, key] = hash.split(":");
        const derived = scryptSync(password, salt, 64, { N: 16384, r: 16, p: 1 });
        return timingSafeEqual(Buffer.from(key, "hex"), derived);
      },
    },
  },
});

This uses the same parameters and format as the built-in implementation [4], so existing hashes remain compatible. bcryptjs is another option if you'd prefer a different algorithm [5].

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4162741679 --> @dosubot[bot] commented on GitHub (Mar 31, 2026): <!-- Greeting --> Hi @chrisdadev13! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known problem — the pure-JS scrypt from `@noble/hashes` with `N=16384, r=16, p=1` consistently takes ~4.5–5s of CPU time on Cloudflare Workers, which is right at or over their CPU budget [[1]](https://github.com/better-auth/better-auth/issues/969#issuecomment-3024999773). [PR #8685](https://github.com/better-auth/better-auth/pull/8685) (`perf(core): use non-blocking scrypt from @better-auth/utils`) is currently open and awaiting review — it directly addresses this issue [[2]](https://github.com/better-auth/better-auth/issues/8685). In the meantime, the workaround you mentioned works well. For anyone else hitting this, you can override the hash/verify functions in your config [[3]](https://github.com/better-auth/better-auth/blob/75133eb478e77c991de67ce25e9ae38ee520c6b3/packages/better-auth/src/context/create-context.ts#L340-L348): ```typescript import { scryptSync, randomBytes, timingSafeEqual } from "node:crypto"; export const auth = betterAuth({ emailAndPassword: { enabled: true, password: { hash: async (password) => { const salt = randomBytes(16).toString("hex"); const key = scryptSync(password, salt, 64, { N: 16384, r: 16, p: 1 }); return `${salt}:${key.toString("hex")}`; }, verify: async ({ hash, password }) => { const [salt, key] = hash.split(":"); const derived = scryptSync(password, salt, 64, { N: 16384, r: 16, p: 1 }); return timingSafeEqual(Buffer.from(key, "hex"), derived); }, }, }, }); ``` This uses the same parameters and format as the built-in implementation [[4]](https://github.com/better-auth/better-auth/blob/75133eb478e77c991de67ce25e9ae38ee520c6b3/packages/better-auth/src/crypto/password.ts), so existing hashes remain compatible. `bcryptjs` is another option if you'd prefer a different algorithm [[5]](https://github.com/better-auth/better-auth/pull/6693). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/a74025c2-1186-4ad7-a4a9-4b24b217ae81?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a74025c2-1186-4ad7-a4a9-4b24b217ae81?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a74025c2-1186-4ad7-a4a9-4b24b217ae81?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a74025c2-1186-4ad7-a4a9-4b24b217ae81?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a74025c2-1186-4ad7-a4a9-4b24b217ae81?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a74025c2-1186-4ad7-a4a9-4b24b217ae81?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/a74025c2-1186-4ad7-a4a9-4b24b217ae81?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20problem%20%E2%80%94%20the%20pure-JS%20scrypt%20from%20%60%40noble/hashes%60%20with%20%60N%3D16384%2C%20r%3D16%2C%20p%3D1%60%20consistently%20takes%20~4.5%E2%80%935s%20of%20CPU%20time%20on%20Cloudflare%20Workers%2C%20which%20is%20right%20at%20or%20over%20their%20CPU%20budget%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/issues/969%23issuecomment-3024999773%29.%0A%0A%5BPR%20%238685%5D%28https%3A//github.com/better-auth/better-auth/pull/8685%29%20%28%60perf%28core%29%3A%20use%20non-blocking%20scrypt%20from%20%40better-auth/utils%60%29%20is%20currently%20open%20and%20awaiting%20review%20%E2%80%94%20it%20directly%20addresses%20this%20issue%20%5B%5B2%5D%5D%28https%3A//github.com/better-auth/better-auth/issues/8685%29.%0A%0AIn%20the%20meantime%2C%20the%20workaround%20you%20mentioned%20works%20well.%20For%20anyone%20else%20hitting%20this%2C%20you%20can%20override%20the%20hash/verify%20functions%20in%20your%20config%20%5B%5B3%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/75133eb478e77c991de67ce25e9ae38ee520c6b3/packages/better-auth/src/context/create-context.ts%23L340-L348%29%3A%0A%0A%60%60%60typescript%0Aimport%20%7B%20scryptSync%2C%20randomBytes%2C%20timingSafeEqual%20%7D%20from%20%22node%3Acrypto%22%3B%0A%0Aexport%20const%20auth%20%3D%20betterAuth%28%7B%0A%20%20emailAndPassword%3A%20%7B%0A%20%20%20%20enabled%3A%20true%2C%0A%20%20%20%20password%3A%20%7B%0A%20%20%20%20%20%20hash%3A%20async%20%28password%29%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20salt%20%3D%20randomBytes%2816%29.toString%28%22hex%22%29%3B%0A%20%20%20%20%20%20%20%20const%20key%20%3D%20scryptSync%28password%2C%20salt%2C%2064%2C%20%7B%20N%3A%2016384%2C%20r%3A%2016%2C%20p%3A%201%20%7D%29%3B%0A%20%20%20%20%20%20%20%20return%20%60%24%7Bsalt%7D%3A%24%7Bkey.toString%28%22hex%22%29%7D%60%3B%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20verify%3A%20async%20%28%7B%20hash%2C%20password%20%7D%29%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20%5Bsalt%2C%20key%5D%20%3D%20hash.split%28%22%3A%22%29%3B%0A%20%20%20%20%20%20%20%20const%20derived%20%3D%20scryptSync%28password%2C%20salt%2C%2064%2C%20%7B%20N%3A%2016384%2C%20r%3A%2016%2C%20p%3A%201%20%7D%29%3B%0A%20%20%20%20%20%20%20%20return%20timingSafeEqual%28Buffer.from%28key%2C%20%22hex%22%29%2C%20derived%29%3B%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%2C%0A%7D%29%3B%0A%60%60%60%0A%0AThis%20uses%20the%20same%20parameters%20and%20format%20as%20the%20built-in%20implementation%20%5B%5B4%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/75133eb478e77c991de67ce25e9ae38ee520c6b3/packages/better-auth/src/crypto/password.ts%29%2C%20so%20existing%20hashes%20remain%20compatible.%20%60bcryptjs%60%20is%20another%20option%20if%20you%27d%20prefer%20a%20different%20algorithm%20%5B%5B5%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/6693%29.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/8860)
Author
Owner

@bytaesu commented on GitHub (Mar 31, 2026):

Hi @chrisdadev13, thanks for letting us know!

I had noticed this issue on CF Workers as well, but assumed it was unavoidable since hashing is inherently slow.

I'll finalize my PR and include it in the next release.

<!-- gh-comment-id:4166458431 --> @bytaesu commented on GitHub (Mar 31, 2026): Hi @chrisdadev13, thanks for letting us know! I had noticed this issue on CF Workers as well, but assumed it was unavoidable since hashing is inherently slow. I'll finalize my PR and include it in the next release.
Author
Owner

@chrisdadev13 commented on GitHub (Apr 1, 2026):

Hi @chrisdadev13, thanks for letting us know!

I had noticed this issue on CF Workers as well, but assumed it was unavoidable since hashing is inherently slow.

I'll finalize my PR and include it in the next release.

Amazing!! Thank you so much 🙏🚀

<!-- gh-comment-id:4166637826 --> @chrisdadev13 commented on GitHub (Apr 1, 2026): > Hi [@chrisdadev13](https://github.com/chrisdadev13), thanks for letting us know! > > I had noticed this issue on CF Workers as well, but assumed it was unavoidable since hashing is inherently slow. > > I'll finalize my PR and include it in the next release. Amazing!! Thank you so much 🙏🚀
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19848