← All articles

Password Hashing in PHP in 2026: A Simple, Safe Guide

Another password hashing guide in 2026? Fair question. Password leaks still make headlines, and old code examples still get copied. Apparently, passwords have not read the memo about retiring.

The good news is that PHP gives you the tools to store passwords safely. You do not need to invent a hashing system yourself.

The short version: Use password_hash() to store a password, password_verify() to check it, and let PHP create the salt.

Why hash passwords?

A password hash is a value made from a password using a one-way process. Your app stores the hash, not the original password. When someone signs in, PHP checks the password they entered against the stored hash.

This gives users some protection if a database is exposed. An attacker who gets the database does not immediately get everyone's passwords. Weak passwords can still be guessed, so the hashing method matters.

Why not MD5 or SHA-256?

MD5 and SHA-256 run quickly. That makes them useful for some tasks, but it also lets an attacker try huge numbers of password guesses quickly after a database leak. A unique salt stops some shortcuts, but it does not make each guess slow enough.

Password hashing algorithms such as Argon2id and bcrypt are designed to make guessing more expensive. SHA-256 is still useful for other jobs. It is simply a poor choice for storing passwords on its own.

PHP has supported password hashing since 5.5

password_hash() arrived in PHP 5.5. It creates a password hash and includes the information needed to check it later. PHP also provides password_verify() for sign-in checks and password_needs_rehash() for upgrading stored hashes over time.

Let PHP handle the salt

A salt is a unique random value added during hashing. It helps ensure that two accounts with the same password do not end up with the same stored hash.

Do not pass a custom salt option to password_hash(). That option was deprecated in PHP 7. In PHP 8, an explicitly supplied salt is ignored. PHP generates a suitable salt automatically, so you can leave that option out.

You do not need a separate database column for the salt. The complete value returned by password_hash() contains the information password_verify() needs.

Which algorithm should you use?

For a new system, Argon2id is a strong choice when your PHP installation supports it and you have tested its memory and speed settings on your server. PHP exposes it as PASSWORD_ARGON2ID when Argon2 support is available.

PASSWORD_DEFAULT is a straightforward built-in option. As of 2026, it uses bcrypt, though PHP may change the default in a future release. Store the complete hash in a column large enough for future algorithms. A VARCHAR(255) column is a practical choice.

Bcrypt uses only the first 72 bytes of a password. Bytes are not always the same as characters, especially for passwords containing non-English letters or emoji. If you want to support longer passwords, consider Argon2id.

Keep hashes up to date

Hash settings can change as hardware improves. After a successful sign-in, password_needs_rehash() can tell you whether a stored hash should be replaced. Your app can then hash the password the user just entered and save the new result.

Use the same algorithm and options when creating a hash and checking whether it needs an update. This lets you improve stored hashes as people return without asking everyone to reset their password at once.

A quick checklist

  • Store the full password hash, never the plain password.
  • Use password_hash() and password_verify().
  • Let PHP generate the salt.
  • Use a database column that can hold at least 255 characters.
  • Use prepared statements when saving or updating hashes.
  • Review your algorithm and cost settings as your server changes.

For more detail, see the PHP password_hash() manual, the password_needs_rehash() manual, and the OWASP Password Storage Cheat Sheet.

A login example

This example assumes $pdo is an existing PDO connection and the users table has id, email, and password_hash columns. It uses the current bcrypt-based PASSWORD_DEFAULT, so the same 72-byte password limit must also be applied when users sign up. A real login endpoint also needs HTTPS and rate limiting.

<?php

session_start();

$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if($email === false || $email === null || strlen($email) > 254) {
    http_response_code(400);
    exit('Email empty or invalid!');
}

$password = $_POST['password'] ?? null;
if(!is_string($password) || $password === '' || strlen($password) > 72) {
    http_response_code(400);
    exit('Password empty or invalid!');
}

$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

if(!$user || !password_verify($password, $user['password_hash'])) {
    http_response_code(401);
    exit('Invalid email or password.');
}

if(password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
    $newHash = password_hash($password, PASSWORD_DEFAULT);

    $update = $pdo->prepare('UPDATE users SET password_hash = :hash WHERE id = :id');
    $update->execute([
        'hash' => $newHash,
        'id' => $user['id'],
    ]);
}

session_regenerate_id(true);
$_SESSION['user_id'] = (int)$user['id'];

header('Location: /account');
exit;

That is the whole idea: store a hash, check it at login, and update it when needed. Let PHP handle the salt, and keep real passwords out of logs, emails, and debugging tools. A few careful choices now can save a lot of trouble after the next leak makes headlines.