TechEarl

What Column Type Should a Password Be in MySQL?

The column type for a password in MySQL is VARCHAR(255). You store the encoded output of a slow password hash (bcrypt, argon2id, scrypt), never a raw MD5 or SHA-256, and you never query the table by password.

Ishan Karunaratne⏱️ 11 min readUpdated
Share thisCopied
What column type a password should be in MySQL: VARCHAR(255) holding the encoded bcrypt or argon2id hash, why CHAR(60) and VARCHAR(60) and TEXT are wrong, and the lookup-then-verify flow.

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:

AlgorithmTypical encoded lengthNotes
bcrypt ($2y$)60 chars (fixed)Length never varies, even as cost rises
argon2i / argon2id~95–96 charsGrows slightly with larger m/t/p parameters
scrypt (libsodium)up to ~128 charsDepends on the parameter encoding
PHP PASSWORD_DEFAULT60 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.

sql
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:

sql
-- 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:

php
// 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):

python
from passlib.hash import argon2

stored = argon2.hash(plaintext)          # store in VARCHAR(255)
ok = argon2.verify(plaintext, stored)    # True / False on login

Node uses the bcrypt package (or argon2):

javascript
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 login

In 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

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsMySQLPasswordsbcryptargon2idVARCHARSchema DesignHashingSecurity

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Store an Argon2 Password Hash in MySQL

Store an Argon2id password hash in MySQL or MariaDB the right way: VARCHAR(255), never a fixed-width column. The encoded format, why its length varies, computing it in PHP / Python / Node, OWASP parameters, and a worked users schema.

How to Store a Phone Number in MySQL

Store a phone number in MySQL as a string, never an integer. Normalize to E.164 and use VARCHAR(16), index it for lookups, and keep the raw input in a second column. Worked schema for MySQL and MariaDB.

How to Store a bcrypt Password Hash in PostgreSQL

A bcrypt hash is a fixed 60-character string. In PostgreSQL the right column is text (varchar(255) is equivalent). Why text over char(60), app-side hashing, the pgcrypto crypt() option, and a worked users schema.