First, it makes sense to understand which scenarios/attacks you're intending to handle:  

- Valid user innocently mistypes the wrong password
- Valid user really forgot his password - and is willing to manually try anything he can think of
- Attacker is manually trying to guess someone else's password
- Attacker is using automated tool to brute-force a specific user's password
- Attacker is using automated tool to brute-force any possible user's password (i.e. breadth-first search)
- Attacker wants to block a specific user from accessing the site
- Attacker wants to block most users from accessing the site
- Attacker wants to block administrators from accessing the site
- Attacker wants to create a lot of manual work, or cost your org a lot in some other way.

(See? Not so simple is it...) And there are different solutions for each part of these... 

- Do not reveal a list of usernames. Don't make it any easier... This includes not revealing if the username is valid or not, when refusing the login.
- Require a strong password policy - long and complex.
- Make the "forgot password" mechanism easy and noticable. (Of course, make sure that's secure too).
- Each time a login request fails, log this to the database. Make sure you write the username, IP address, time, etc.
- If numerous failed requests are received for a specific username, mark that user in the database as locked for a short time. Also encourage the user to use the "forgot password" feature, if this is in the same session.
- How many failed requests? [It depends][1]. In short, whatever makes sense for your site, the exact number is not critical.
- How long should the user be locked for? Short intervals. Mathematically, it doesn't really matter, as long as you have a strong password policy. Realistically, start with very short interval of a few minutes, then if it continues make it incrementally longer. E.g. after 5 bad tries, lock for 5 minutes; after another 3 bad tries, lock for 15; after another 2, lock for 30; etc.
- Do not lock user accounts permanently. This leads to Account DoS. And, can also cost your support personell a lot of time and money.
- Despite the previous point/s, if your site is veryveryvery sensitive, e.g. a banking app, you might want to consider locking permanently till further notice, e.g. have the customer come into his branch.
- Locks should be by username, on the database. **Not** by sessionId, and not in the webserver session.
- If you receive many failed requests with different usernames, but from the same IP address - implement incremental locking like above, but with wider grace and shorter intervals. 
- Provide a "forgot username" feature, in addition to the forgot password. 
- Administrator accounts should have shorter grace, longer intervals, and never permanently lock them out.  
- In any event, when locking a user or IP, send an alert to administrators. Not for repeated locking, though - you don't want to flood the admin's inbox. But if it does continue, then elevate the alert level after a few times.
- Don't use CAPTCHA - the minimal added difficulty is trivial, in relation to the value of accessing the user's password. (There are many ways around it, CAPTCHA is fundamentally [broken, regardless of implementation][2]).
- Of course, as @rox0r stated, multi-factor authentication might be appropriate for you.  
- Another alternative, is whats known as "*adaptive authentication*" - if the user failed the login, ask for additional information (that had been pre-registered). Depending on additional risk-factors (e.g. location, time patterns different from usual, etc), escalate the information required to successfully authenticate. For example RSA's AdaptiveAuthentication product does this.





  [1]: https://security.stackexchange.com/q/487/33
  [2]: https://security.stackexchange.com/q/778/33