Hashing advice ages badly, and old tutorials stay indexed. These six recommendations still appear regularly and all of them will hurt you.
SHA-256 is designed to be fast, and hardware accelerates it further. That is exactly wrong for passwords: an attacker with a stolen hash database wants to try as many candidates per second as possible, and a fast hash hands them that. Password hashing needs a deliberately slow, memory-hard function. Use Argon2id (OWASP suggests starting around 19 MiB of memory with 2 iterations), or bcrypt with a cost factor tuned so a single hash takes roughly 100–250 ms on your production hardware, or scrypt where those are unavailable.
A salt is not a secret. Its only job is to make each hash unique so one precomputed rainbow table cannot attack every account at once, and so two users with the same password get different hashes. Salts are stored alongside the hash — every modern format embeds them in the output string, which is why a bcrypt hash is 60 characters containing the algorithm, cost, salt and digest together. A per-user random salt of 16 bytes from a CSPRNG is correct. What you may additionally want is a pepper: a secret held outside the database, applied via HMAC before hashing. Different thing, different storage, and only useful if it genuinely lives somewhere the database dump would not reach.
Applying SHA-256 twice, or chaining MD5 into SHA-1, does not add meaningful strength and can reduce it. Composing primitives yourself invites length-extension issues, collision transfer and implementation mistakes, and it makes your scheme non-standard so no library can verify it. One well-chosen algorithm with correct parameters beats any homemade chain. If you need a keyed hash, use HMAC — which exists precisely because naively prefixing a key to a message and hashing is broken for Merkle–Damgård constructions.
Half true and worth being precise about. MD5 is thoroughly broken for anything where an adversary chooses the input — collisions can be produced in seconds. For a non-adversarial checksum, such as detecting accidental corruption in a file you control, it still functions. But there is no reason to choose it: BLAKE3 is faster than MD5 on modern hardware and is not broken, and xxHash is faster still for non-cryptographic bucketing. The risk with keeping MD5 around is that a non-security use case quietly becomes a security one when someone reuses the function for deduplication of user uploads.
Cutting a 256-bit digest to 64 bits reduces collision resistance from about 2^128 to about 2^32 by the birthday bound, which is roughly four billion — reachable on a laptop. If you are using the hash as a content identifier and truncate it, you will eventually get two different files mapping to the same key, and the failure will be silent data corruption. Storage is cheap; 32 bytes per row is not the optimisation worth making. Where truncation is acceptable it is specified as a distinct algorithm, such as SHA-512/256, not as an ad hoc slice.
Standard string comparison returns as soon as it finds a differing byte, so the time it takes leaks how many leading bytes matched. Over enough requests that is enough to reconstruct a token or signature byte by byte. Use a constant-time comparison: hmac.compare_digest in Python, crypto.timingSafeEqual in Node, hmac.Equal in Go, hash_equals in PHP. It is a one-word change and it closes a real, demonstrated attack class.
Passwords get Argon2id or bcrypt with measured parameters and per-user salts. Message authentication gets HMAC-SHA-256 with a constant-time comparison. File integrity and content addressing get SHA-256 or BLAKE3. Non-security bucketing gets xxHash. Nothing gets MD5, SHA-1, a homemade chain, or a truncated digest.