Scrypt asks for memory it does not need.
That sounds like a flaw and it is the design. Custom cracking hardware is cheap to build for arithmetic and expensive to build for RAM, so a function that demands a large block of memory per guess raises an attacker's cost in a way that iteration counts cannot.
Everything here runs locally
The parameter that surprises people
Memory use is roughly 128 × N × r bytes. Both numbers matter, and changing only N gives you a fraction of the memory the number suggests. The readout above updates as you change either one, so you can see what a setting actually asks for before you put it into production.
- N = 2^14, r = 8
- 16 MiB per hash
- N = 2^17, r = 8
- 128 MiB per hash
- N = 2^17, r = 1
- 16 MiB per hash
- N = 2^20, r = 8
- 1 GiB per hash
The middle two rows share the same N and differ by a factor of eight in memory. That is the trap.
In your own code
Node.js
import { scrypt, randomBytes } from 'node:crypto';
const salt = randomBytes(16);
scrypt(password, salt, 32, { N: 2 ** 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 }, (err, key) => {
// store salt and key together with the parameters
});Python
import hashlib, os
salt = os.urandom(16)
key = hashlib.scrypt(password.encode(), salt=salt, n=2**17, r=8, p=1, dklen=32)Go
import "golang.org/x/crypto/scrypt"
key, err := scrypt.Key([]byte(password), salt, 1<<17, 8, 1, 32)Node enforces a maxmem ceiling of 32 MiB by default and throws once your parameters go past it, which is the usual reason a working local script fails the first time N goes up.