The column type for a password in MySQL is VARCHAR(255). You don't store the password itself, and you don't store a fast hash of it either. You store the encoded output of a slow password-hashing function (bcrypt, argon2id, or scrypt), which is a self-describing ASCII string that already bakes in the algorithm, the cost parameters, and the salt. VARCHAR(255) fits every one of those formats with room to spare, and it survives an algorithm upgrade later without an ALTER TABLE.
Short answer: use password_hash VARCHAR(255) NOT NULL (store it as ascii or latin1, since the encoded hash is pure ASCII), compute the hash in your application with a real password library, never with MySQL's MD5() or SHA2(), and never write WHERE password = .... You look up the user row by their email or username, pull the stored hash out, and verify the candidate password against it in app code. This is the opposite of the integrity-hash story in storing a SHA-256 hash in MySQL: SHA-256 is for fingerprinting, not credentials.
Why VARCHAR(255) and not something shorter
The instinct is to size the column to exactly fit the hash you use today. bcrypt output is exactly 60 characters, so CHAR(60) looks tidy. The problem is that you are not really storing a bcrypt hash. You are storing whatever your password library hands back, and that format changes the day you raise the cost factor, switch algorithms, or move to a new library version.
The encoded string is self-describing. A bcrypt hash looks like $2y$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW: the $2y$ is the algorithm identifier, 12 is the cost, and the rest is the salt and digest packed together. argon2id looks like $argon2id$v=19$m=65536,t=4,p=1$...$..., encoding the memory, time, and parallelism parameters inline. Because the parameters travel with the hash, you can verify an old password against an old hash even after you have moved everyone new onto stronger settings. That is the whole point, and it only works if the column is wide enough to hold the longest format you might ever migrate to.
Here are the encoded lengths in practice:
| Algorithm | Typical encoded length | Notes |
|---|---|---|
bcrypt ($2y$) | 60 chars (fixed) | Length never varies, even as cost rises |
| argon2i / argon2id | ~95–96 chars | Grows slightly with larger m/t/p parameters |
| scrypt (libsodium) | up to ~128 chars | Depends on the parameter encoding |
PHP PASSWORD_DEFAULT | 60 today (bcrypt) | Could change in a future PHP release |
VARCHAR(255) covers all of these with comfortable headroom, and 255 is not an arbitrary number: it is the largest length a VARCHAR can have while still using a single-byte length prefix (up to 255 bytes), so you pay one byte of overhead plus the actual hash bytes and nothing more. VARCHAR is variable-length, so a 60-character bcrypt hash still only occupies 61 bytes on disk in this column. You get the migration headroom for free.
This is also the recommendation you will find everywhere that has thought about it. PHP's manual tells you to use VARCHAR(255) for the result of password_hash(). Laravel's default users migration ships a password column as string, which maps to VARCHAR(255). Django stores its password field as a 128-character field but the principle is the same: width that outlives the current algorithm. WordPress is a concrete case study in why: it kept its user_pass column at VARCHAR(255), and that one decision let it switch its hashing from phpass to bcrypt decades later with no schema change. MariaDB is identical to MySQL here in every respect: same types, same sizes, same advice.
What NOT to use
A few column types look reasonable and are wrong for specific reasons.
CHAR(60) sizes the column to bcrypt exactly. It works, today, for bcrypt only. The moment you decide argon2id is the better default (it is, for new systems), every new hash is ~96 characters and no longer fits. Now you need an ALTER TABLE ... MODIFY on your users table, which on a large table is a locking, blocking, multi-minute operation you have to schedule. CHAR also pads to the full width, so even if every value were 60 chars you save nothing over VARCHAR in practice, and you lose the headroom.
VARCHAR(60) has the same fatal flaw as CHAR(60): it is sized to one algorithm. The variable-length saving over CHAR(60) is real but irrelevant, because the column is too narrow to hold the next algorithm you will want. Don't tie your schema to bcrypt's exact output length.
TEXT is over-correction in the other direction. The encoded hash is at most ~128 bytes, so a TEXT column (built for kilobyte-to-gigabyte blobs) is pure waste, and it has real downsides: TEXT columns cannot have a useful DEFAULT, InnoDB may store long TEXT values off-page in overflow pages, and you cannot index a TEXT column without a prefix length. None of that helps a fixed, short, ASCII credential. VARCHAR(255) is the right shape.
A raw MD5() or SHA2() column. This is the dangerous one, because it is the most common. MD5 and SHA-256 are fast hashes, which is exactly what you do not want for a password: an attacker who steals your table can try billions of guesses per second against a fast hash on a single GPU. They are also unsalted by default, so identical passwords produce identical hashes and a precomputed rainbow table cracks them instantly. Storing SHA2(password, 256) is only marginally better than storing MD5, which is to say not good enough. If you have an existing table doing this, read why MD5 is the wrong choice for passwords and plan a reset-on-next-login migration. SHA-256 has its place, but that place is integrity and fingerprinting, never credentials.
A complete users-table schema
Here is a users table done correctly. The password column is VARCHAR(255); the lookup key is a UNIQUE email; the password column is never part of any index or WHERE clause used to find a user.
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB;The CHARACTER SET ascii on password_hash matters: every byte a password library emits is a printable ASCII character ($, ., /, [A-Za-z0-9]), so an ascii or latin1 column stores it in one byte each. If your table default is utf8mb4, leaving this column on utf8mb4 reserves up to 4 bytes per character in any index calculation, tripling the budget for zero benefit. You typically do not index password_hash at all, but pinning it to ascii keeps the column honest and avoids surprises. The ascii_bin collation also makes the stored hash compare byte-for-byte, which is correct, though you should never be comparing it in SQL anyway.
The lookup-then-verify flow is the part people get backwards. You do not ask the database "is there a row where the password matches?" That would require sending the candidate password to the database and comparing it server-side, which is impossible once the stored value is a salted hash (the salt differs per row, so equality never holds). Instead:
-- 1. Find the user by their identifier, pull the stored hash.
SELECT id, password_hash FROM users WHERE email = ?;
-- 2. Verify the candidate password against that hash IN YOUR APPLICATION.
-- There is no step where the password appears in a WHERE clause.If the SELECT returns no row, authentication fails (and you should still run a dummy verify to keep response timing constant, so you do not leak which emails exist). If it returns a row, you hand the candidate password and the stored hash to your password library and let it tell you yes or no.
Hashing in your application
The hash is always computed in the application layer, never in MySQL. Every mainstream language ships a battle-tested library.
PHP has this built in. PASSWORD_DEFAULT is bcrypt today; PASSWORD_ARGON2ID is available since PHP 7.3 if your build includes libargon2:
// On registration / password change:
$hash = password_hash($plaintext, PASSWORD_DEFAULT); // 60-char bcrypt string
// or, for argon2id:
$hash = password_hash($plaintext, PASSWORD_ARGON2ID); // ~96-char string
// Store $hash in users.password_hash (VARCHAR(255)).
// On login, after SELECTing the row by email:
if (password_verify($plaintext, $row['password_hash'])) {
// authenticated
if (password_needs_rehash($row['password_hash'], PASSWORD_DEFAULT)) {
// re-hash with current params and UPDATE the row
}
}Python uses passlib (or the bcrypt / argon2-cffi packages directly):
from passlib.hash import argon2
stored = argon2.hash(plaintext) # store in VARCHAR(255)
ok = argon2.verify(plaintext, stored) # True / False on loginNode uses the bcrypt package (or argon2):
const bcrypt = require("bcrypt");
const hash = await bcrypt.hash(plaintext, 12); // cost factor 12
// store hash in users.password_hash
const ok = await bcrypt.compare(plaintext, storedHash); // on loginIn all three, the library generates a fresh random salt per call, folds it and the cost parameters into the returned string, and the verify function pulls them back out to re-derive and compare in constant time. You store one string, you verify against one string, and the column never participates in the lookup. For the algorithm-specific deep dives, see the dedicated guides on storing a bcrypt hash in MySQL and storing an Argon2id hash in MySQL.
What to do next
- For every MySQL data type with exact byte sizes and the
VARCHARlength-prefix rules, see MySQL Data Types and Sizes. - For the concrete algorithms that fill this column, see storing a bcrypt hash in MySQL and the memory-hard Argon2id hash in MySQL.
- For why a fast hash is the wrong tool here, see when MD5 is the wrong choice.
- For the integrity-hash counterpart (SHA-256 for checksums and fingerprints, not passwords), see how to store a SHA-256 hash in MySQL.
FAQ
See also
- Storing a bcrypt hash in PostgreSQL covers the same credential column on the other major open-source database.
- How WordPress stores passwords is the real-world case study for why a wide
VARCHARcolumn outlives any single hashing scheme. - The MySQL data types and sizes reference has the exact byte costs and the
VARCHARlength-prefix rules behind the 255 boundary. - If you do need to widen an existing column, changing a column type in MySQL walks through the locking
ALTER TABLEyou are trying to avoid. - Keep the day-to-day syntax handy with the MySQL cheat sheet.
- To test any of this on a throwaway instance, see running MySQL in Docker.
Sources
Authoritative references this article was fact-checked against.
- PHP Manual: password_hash() (recommended storage VARCHAR(255), PASSWORD_DEFAULT, PASSWORD_ARGON2ID)php.net
- PHP Manual: password_verify()php.net
- Laravel Documentation: Migrations (string password column)laravel.com
- MySQL 8.0 Reference Manual: String Data Types (CHAR, VARCHAR, TEXT)dev.mysql.com
- RFC 9106: Argon2 Memory-Hard Function for Password Hashingdatatracker.ietf.org





