diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 58112a67131a..06b284bde965 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -21,3 +21,4 @@ extends:
 rules:
   # For overloads.
   no-dupe-class-members: off
+  "@typescript-eslint/no-use-before-define": off
diff --git a/doc/FAQ.md b/doc/FAQ.md
index 6da4fdcc72b1..ec0acf934af0 100644
--- a/doc/FAQ.md
+++ b/doc/FAQ.md
@@ -52,6 +52,8 @@ randomly generated password so you can use that. You can set the `PASSWORD` envi
 to use your own instead. If you want to handle authentication yourself, use `--auth none`
 to disable password authentication.
 
+**note**: code-server will rate limit password authentication attempts at 2 a minute and 12 an hour.
+
 If you want to use external authentication you should handle this with a reverse
 proxy using something like [oauth2_proxy](https://github.com/pusher/oauth2_proxy).
 
diff --git a/package.json b/package.json
index 0bf8d2a8dac6..b5c73a391256 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
     "fs-extra": "^8.1.0",
     "http-proxy": "^1.18.0",
     "httpolyglot": "^0.1.2",
+    "limiter": "^1.1.5",
     "node-pty": "^0.9.0",
     "pem": "^1.14.2",
     "safe-compare": "^1.1.4",
diff --git a/src/node/app/login.ts b/src/node/app/login.ts
index b55f5503cbb6..c2dc707c22f0 100644
--- a/src/node/app/login.ts
+++ b/src/node/app/login.ts
@@ -1,4 +1,5 @@
 import * as http from "http"
+import * as limiter from "limiter"
 import * as querystring from "querystring"
 import { HttpCode, HttpError } from "../../common/http"
 import { AuthType, HttpProvider, HttpResponse, Route } from "../http"
@@ -48,6 +49,8 @@ export class LoginHttpProvider extends HttpProvider {
     return this.replaceTemplates(route, response)
   }
 
+  private readonly limiter = new RateLimiter()
+
   /**
    * Try logging in. On failure, show the login page with an error.
    */
@@ -59,6 +62,10 @@ export class LoginHttpProvider extends HttpProvider {
     }
 
     try {
+      if (!this.limiter.try()) {
+        throw new Error("Login rate limited!")
+      }
+
       const data = await this.getData(request)
       const payload = data ? querystring.parse(data) : {}
       return await this.login(payload, route, request)
@@ -108,3 +115,17 @@ export class LoginHttpProvider extends HttpProvider {
     throw new Error("Missing password")
   }
 }
+
+// RateLimiter wraps around the limiter library for logins.
+// It allows 2 logins every minute and 12 logins every hour.
+class RateLimiter {
+  private readonly minuteLimiter = new limiter.RateLimiter(2, "minute")
+  private readonly hourLimiter = new limiter.RateLimiter(12, "hour")
+
+  public try(): boolean {
+    if (this.minuteLimiter.tryRemoveTokens(1)) {
+      return true
+    }
+    return this.hourLimiter.tryRemoveTokens(1)
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index c259a40e6002..199d4aa84234 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4044,6 +4044,11 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+limiter@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
+  integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==
+
 lines-and-columns@^1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"