The reasons are a short list and you can usually identify yours in a couple of minutes.
A function or a cast on the indexed column. If the query says something like lower of a column, or the column cast to a different type, the index on the plain column cannot be used: the planner would have to evaluate the function on every row anyway. This is the most common cause by a distance, and the cast version is sneaky because it is often implicit, inserted because the parameter type does not match the column type.
A leading wildcard in a pattern match. An index can find things that start with something; it cannot find things that end with something.
The wrong column order in a composite index. An index on two columns can serve a query filtering on the first, or on both, but generally not on the second alone. If your filter uses them in the other order than the index declares, it is not usable.
The planner thinks a scan is cheaper. If the filter matches a large fraction of the table, reading it all sequentially genuinely is faster, and the planner is right. This is the case where nothing is wrong.
Stale statistics, covered below.
A type mismatch between the column and the parameter, particularly with an ORM binding a parameter as text against a non-text column.