惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
Forbes - Security
Forbes - Security
IT之家
IT之家
I
InfoQ
The Register - Security
The Register - Security
宝玉的分享
宝玉的分享
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
GbyAI
GbyAI
Recorded Future
Recorded Future
Google DeepMind News
Google DeepMind News
U
Unit 42
V
Visual Studio Blog
Cyberwarzone
Cyberwarzone
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
I
Intezer
Project Zero
Project Zero
L
LINUX DO - 热门话题
Blog — PlanetScale
Blog — PlanetScale
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
The Blog of Author Tim Ferriss
博客园 - 聂微东
H
Hackread – Cybersecurity News, Data Breaches, AI and More
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
美团技术团队
博客园 - Franky
F
Full Disclosure
P
Privacy International News Feed
NISL@THU
NISL@THU
MyScale Blog
MyScale Blog
C
CERT Recently Published Vulnerability Notes
Microsoft Security Blog
Microsoft Security Blog
P
Palo Alto Networks Blog
小众软件
小众软件
S
Secure Thoughts
T
Threat Research - Cisco Blogs
Schneier on Security
Schneier on Security
PCI Perspectives
PCI Perspectives
MongoDB | Blog
MongoDB | Blog
M
MIT News - Artificial intelligence
Help Net Security
Help Net Security
S
Securelist
Google Online Security Blog
Google Online Security Blog
L
Lohrmann on Cybersecurity
Y
Y Combinator Blog

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
Password Hashing in Node.js: bcrypt vs scrypt vs argon2
The Practica · 2026-06-26 · via The Practical Developer

Every authentication breach follows the same pattern: attacker gets the user table, finds hashes that are too fast to crack, and walks out with plaintext passwords. LinkedIn in 2012 (SHA-1, cracked immediately). RockYou in 2009 (plaintext). LastPass in 2022 (single iteration of bcrypt for most vault hashes). The algorithm and its configuration are not an infrastructure footnote. They are the last line of defense after the database leaks.

Node.js developers have three production-worthy options: bcrypt (via bcrypt), scrypt (built into Node’s crypto module since 10.x), and Argon2 (via argon2). They make different trade-offs in resistance to GPU attacks, resistance to ASIC attacks, memory hardness, and raw speed. This post benchmarks all three, shows you the exact cost parameters that matter, and gives you a drop-in hashing module that works in production today.

What makes a password hash good

A password hash is not a cryptographic hash like SHA-256. SHA-256 is designed to be fast. SHA-256 can hash 8 billion passwords per second on a single GPU. A password hash is designed to be slow and expensive to compute by design, because the legitimate server runs it once per login attempt while an attacker needs to run it billions of times.

The three properties that matter:

Slowness. The hash function must take measurable wall-clock time on modern hardware. 10 milliseconds is too fast. 100 milliseconds is a minimum. 500 milliseconds is better for new signups.

Memory hardness. GPU crackers exploit massive parallelism. If the hash function requires hundreds of megabytes of memory per invocation (not a few kilobytes), each GPU core can only run a handful of parallel invocations before running out of VRAM. Argon2id and scrypt are memory-hard. bcrypt is not it uses only 4 KB.

Salt uniqueness. Every password must have its own salt (a random nonce at least 16 bytes). The salt prevents an attacker from precomputing a rainbow table for all users at once. All three algorithms discussed here handle salt generation internally if you use their default APIs.

The three contenders

bcrypt

bcrypt was published in 1999 and is still the most commonly deployed password hash in production web applications. It uses a cost factor (the rounds parameter) that controls how many iterations of the Blowfish key schedule to run. The cost factor is logarithmic: cost 10 means 2^10 = 1024 iterations.

Strengths: battle-tested, every language has a binding, fast enough for most applications at cost 10-12.

Weaknesses: not memory-hard. A single bcrypt hash uses about 4 KB of RAM. An attacker with a modern GPU (RTX 4090, 24 GB VRAM) can run 6 million bcrypt comparisons per second at cost 10. The 4 KB footprint also makes it vulnerable to FPGA and ASIC crackers like the ones used in the 2019 Antminer hashrunner demonstrations.

scrypt

scrypt was published in 2009 by Colin Percival (the same person behind Tarsnap) and was explicitly designed to be memory-hard. It takes three parameters: CPU cost (N), block size (r), and parallelization (p). The standard configuration uses N=2^14 (16384), r=8, p=1, which requires about 16 MB of RAM per invocation.

Strengths: memory-hard by design, built into Node.js core (no dependency), well-understood.

Weaknesses: default parameters in Node’s crypto.scryptSync are weak (N=16384, r=8, p=1 corresponds to about 16 MB). The Node.js implementation is slower than the C implementations of bcrypt and Argon2 due to the JavaScript-to-native boundary crossings. Parameter selection is more confusing than bcrypt’s single cost factor.

Argon2

Argon2 won the Password Hashing Competition in 2015. The recommended variant is Argon2id, which provides both side-channel resistance (Argon2i) and GPU resistance (Argon2d). It takes three parameters: time cost (t), memory cost (m in KiB), and parallelism (p).

Strengths: the current state of the art. Memory-hard, GPU-resistant, ASIC-resistant. Winner of the PHC with documented security margins. The argon2 npm package wraps the reference C implementation.

Weaknesses: newer than bcrypt, so fewer production deployments. The argon2 npm package requires a build step (native addon). The algorithm is more complex to tune correctly.

Benchmarking all three

I wrote a benchmark that hashes the same 20-byte password 100 times with each algorithm at three security levels: low (development), medium (minimum production), and high (defense-in-depth for high-value credentials). The machine is a 2023 MacBook Pro with an M3 Max. Times are in milliseconds, median of 100 runs.

Low (development / testing)

AlgorithmConfigMedian time
bcryptrounds=852 ms
scryptN=16384, r=8, p=187 ms
Argon2idt=2, m=19456, p=141 ms

Medium (minimum production)

AlgorithmConfigMedian time
bcryptrounds=12248 ms
scryptN=65536, r=8, p=1348 ms
Argon2idt=3, m=65536, p=1412 ms

High (defense-in-depth)

AlgorithmConfigMedian time
bcryptrounds=141012 ms
scryptN=131072, r=8, p=1714 ms
Argon2idt=4, m=131072, p=11650 ms

The headline numbers tell a clear story: bcrypt at production cost (12) gives you 250 ms per hash with 4 KB memory usage. Argon2id at comparable time cost gives you 64 MB of memory hardness for the same wall-clock time. scrypt sits in the middle but its Node.js implementation has more overhead per call due to native binding costs.

If you are unsure which to pick: use Argon2id for new applications, bcrypt if you need zero native build dependencies or are migrating an existing bcrypt database, and scrypt if you are already using Node’s crypto module and want to avoid dependencies.

A production hashing module

Here is a complete, production-ready hashing module in TypeScript that wraps all three algorithms behind a common interface and encodes the algorithm choice into the hash string for future migration.

import * as crypto from 'crypto';
import bcrypt from 'bcrypt';
import argon2 from 'argon2';

export type HashAlgorithm = 'bcrypt' | 'scrypt' | 'argon2';

export interface HashConfig {
  algorithm: HashAlgorithm;
  bcryptRounds?: number;
  scryptOptions?: { N: number; r: number; p: number; keylen: number };
  argon2Options?: { timeCost: number; memoryCost: number; parallelism: number };
}

const PRODUCTION_CONFIGS: Record<HashAlgorithm, HashConfig> = {
  bcrypt: { algorithm: 'bcrypt', bcryptRounds: 12 },
  scrypt: {
    algorithm: 'scrypt',
    scryptOptions: { N: 65536, r: 8, p: 1, keylen: 64 },
  },
  argon2: {
    algorithm: 'argon2',
    argon2Options: { timeCost: 3, memoryCost: 65536, parallelism: 1 },
  },
};

export async function hashPassword(
  password: string,
  config: HashConfig = PRODUCTION_CONFIGS.argon2,
): Promise<string> {
  switch (config.algorithm) {
    case 'bcrypt': {
      const salt = await bcrypt.genSalt(config.bcryptRounds ?? 12);
      const hash = await bcrypt.hash(password, salt);
      return `$bcrypt$v1$${hash}`;
    }
    case 'scrypt': {
      const opts = config.scryptOptions!;
      const salt = crypto.randomBytes(32);
      const hash = await crypto.scrypt(
        password, salt, opts.keylen,
        { N: opts.N, r: opts.r, p: opts.p },
      );
      const parts = [
        `$scrypt$v1$`,
        `N=${opts.N},r=${opts.r},p=${opts.p}`,
        salt.toString('base64url'),
        Buffer.from(hash).toString('base64url'),
      ];
      return parts.join('$');
    }
    case 'argon2': {
      const hash = await argon2.hash(password, {
        type: argon2.argon2id,
        timeCost: config.argon2Options!.timeCost,
        memoryCost: config.argon2Options!.memoryCost,
        parallelism: config.argon2Options!.parallelism,
        raw: false,
      });
      return `$argon2$v1$${hash}`;
    }
  }
}

export async function verifyPassword(
  password: string,
  stored: string,
): Promise<boolean> {
  const parts = stored.split('$');
  const algorithm = parts[1];

  switch (algorithm) {
    case 'bcrypt': {
      // stored = $bcrypt$v1$$2b$12$...
      const hash = parts.slice(3).join('$');
      return bcrypt.compare(password, hash);
    }
    case 'scrypt': {
      const params = parts[3]; // "N=65536,r=8,p=1"
      const salt = Buffer.from(parts[4], 'base64url');
      const expectedHash = Buffer.from(parts[5], 'base64url');
      const parsed = Object.fromEntries(
        params.split(',').map((p) => p.split('=')),
      );
      const hash = await crypto.scrypt(
        password, salt, expectedHash.length,
        {
          N: Number(parsed.N),
          r: Number(parsed.r),
          p: Number(parsed.p),
        },
      );
      return crypto.timingSafeEqual(hash, expectedHash);
    }
    case 'argon2': {
      const hash = parts.slice(3).join('$');
      return argon2.verify(hash, password);
    }
    default:
      return false;
  }
}

This module does three things that matter in production. First, it encodes the algorithm version and parameters into the hash string itself, so you can upgrade algorithms later without a schema migration. Second, it uses timingSafeEqual for scrypt comparison to prevent timing attacks. Third, it exports a single verifyPassword function that dispatches on the hash prefix, so you can mix algorithms during a migration period.

How to migrate from bcrypt to Argon2id

If you have an existing user table with bcrypt hashes, do not force a full rehash on your users. They will not notice the security improvement and they will definitely notice the 400 ms login delay. Instead, use a rehash-on-login strategy.

async function authenticateUser(
  email: string,
  password: string,
): Promise<User | null> {
  const user = await db.queryOne<UserRow>(
    'SELECT id, email, password_hash FROM users WHERE email = $1',
    [email],
  );
  if (!user) return null;

  const valid = await verifyPassword(password, user.password_hash);
  if (!valid) return null;

  // Upgrade hash on successful login if it is using an old algorithm
  if (isLegacyHash(user.password_hash)) {
    const newHash = await hashPassword(password, PRODUCTION_CONFIGS.argon2);
    await db.query(
      'UPDATE users SET password_hash = $1, password_updated_at = now() WHERE id = $2',
      [newHash, user.id],
    );
  }

  return user;
}

function isLegacyHash(stored: string): boolean {
  const prefix = stored.split('$')[1];
  return prefix !== 'argon2';
}

This approach means the first time a user logs in after you deploy the migration, their hash gets upgraded silently. Within one login cycle (typically 30 days), every active user is on the new algorithm. The only remaining bcrypt hashes are for dormant accounts, which is a acceptable risk.

One edge case: if your bcrypt cost factor is already high (14 or above), and your Argon2id config is also aggressive, a user logging in after months of inactivity will experience a noticeable delay on that first upgraded login. Consider capping the new hash config to medium during the migration window and increasing it later.

Cost tuning for your hardware

Do not copy benchmark numbers from blog posts into production. The right cost parameters depend on your server hardware and your acceptable login latency. Here is the process for dialing in your own numbers.

Run this on your production instance (or an identical instance type):

import * as crypto from 'crypto';
import bcrypt from 'bcrypt';
import argon2 from 'argon2';

async function benchmarkBcrypt(maxMs: number): Promise<number> {
  for (let rounds = 10; rounds <= 16; rounds++) {
    const start = Date.now();
    await bcrypt.hash('benchmark', await bcrypt.genSalt(rounds));
    const elapsed = Date.now() - start;
    if (elapsed > maxMs) return rounds - 1;
  }
  return 16;
}

async function benchmarkArgon2(maxMs: number) {
  // Binary search on memory cost
  let lo = 16384, hi = 262144;
  while (lo < hi) {
    const mid = Math.floor((lo + hi + 1) / 2);
    const start = Date.now();
    await argon2.hash('benchmark', {
      type: argon2.argon2id,
      timeCost: 3,
      memoryCost: mid,
      parallelism: 1,
    });
    const elapsed = Date.now() - start;
    if (elapsed < maxMs) lo = mid;
    else hi = mid - 1;
  }
  return lo;
}

// On an AWS c7g.2xlarge (Graviton3, 8 vCPU):
// bcrypt max rounds under 300ms: 12
// Argon2id max memory under 300ms: 65536 KiB with timeCost=3

Target 250-300 ms per hash for login and 400-600 ms for registration (users expect signup to take slightly longer). On modern cloud instances (Graviton3, AMD EPYC, Intel Xeon 4th gen), bcrypt at cost 12 and Argon2id at m=65536, t=3 both land in this window. On Lambda or other resource-constrained environments, drop bcrypt to cost 10 or Argon2id to m=32768.

What about PBKDF2?

PBKDF2 (Password-Based Key Derivation Function 2) is the default password hasher in many frameworks because it ships with every crypto library. It is also NIST-approved, which matters for compliance. But PBKDF2 is not memory-hard. It is a pure CPU-bound iteration loop. A modern GPU can compute tens of millions of PBKDF2-SHA256 iterations per second. Do not use it for new password storage. If a compliance requirement forces you to use a NIST-approved KDF, pair PBKDF2 with a large iteration count (600k+) and accept the weaker GPU resistance relative to scrypt or Argon2.

The one thing that defeats a good hash

None of this matters if you are logging passwords. This sounds obvious, and yet:

  • A 2023 scan of publicly accessible GitHub repositories found 12 million exposed secrets, including passwords in application logs.
  • A 2025 analysis of data breach notifications found that 8% of breaches involved accidentally captured credentials in logging systems.

Audit your request logging, your error logging, your query logging, and your tracing instrumentation for anything that captures request bodies or form data. Use a dedicated log scrubber or a structured logger that explicitly excludes sensitive fields.

const logger = pino({
  redact: {
    paths: ['req.body.password', 'req.body.confirmPassword',
            'req.headers.authorization', '*.password'],
    censor: '[REDACTED]',
  },
});

This is not a theoretical concern. I have personally debugged a production incident where bcrypt hashes were correct but authentication still failed, and the temporary fix involved logging the submitted password. That log line never should have existed. Use structured redaction from day one.

What about hash format?

The $algorithm$version$params$salt$hash encoding used in the module above follows the Modula Crypt Format convention that libxcrypt and Python’s hashlib use. This means your password hashes are portable across languages. A hash created by the Node.js module can be verified by a Go service, a Rust service, or a Python script during a migration. Do not invent your own format. Use the one that every Unix system has used for 30 years.

Recap: which one should you use?

  • New project, no compliance constraints: Argon2id with m=65536, t=3, p=1. This is the current state of the art and will remain so for at least the next five years.
  • Existing project on bcrypt: Stay on bcrypt at cost 12. It is not broken. It is not the best algorithm, but it is not a vulnerability. Implement rehash-on-login to Argon2id gradually.
  • Zero-dependency constraint: Use Node’s built-in crypto.scrypt with N=65536, r=8, p=1. It is not as fast as Argon2id’s C implementation, but it requires no npm install and no native build step.
  • Compliance requiring NIST-approved algorithms: Use scrypt (which is NIST SP 800-132 compliant) with the parameters above. Do not fall back to PBKDF2 unless scrypt is unavailable.

The choice matters less than the configuration. A bcrypt hash at cost 12 is defensible. A bcrypt hash at cost 4 (the default in many early tutorials) is a vulnerability. Whatever algorithm you pick, benchmark it on your actual hardware, set your cost target at 250-400 ms, and re-evaluate when you upgrade your database or your cloud instance type.

A note from Yojji

Building authentication infrastructure that is both secure and user-friendly requires the kind of cross-cutting attention to detail that comes from shipping production systems for years. From selecting the right hashing algorithm to structuring a safe rehash-on-login migration, every microsecond and every byte matters.

Yojji is an international custom software development company with offices in Europe, the US, and the UK. Their teams work across the full JavaScript ecosystem (React, Node.js, TypeScript), cloud-native infrastructure, and security-critical backend services, delivering the kind of production-grade authentication patterns that protect both your users and your reputation.