diff --git a/src/UserLoginEnforce.php b/src/UserLoginEnforce.php
index d224856..9e0e670 100644
--- a/src/UserLoginEnforce.php
+++ b/src/UserLoginEnforce.php
@@ -13,6 +13,26 @@ class UserLoginEnforce {
 
   use StringTranslationTrait;
 
+  /**
+   * The flood ID for the per-user flood control.
+   */
+  const FLOOD_NAME_UID = 'one_time_password.uid';
+
+  /**
+   * The flood ID for the IP based flood control.
+   */
+  const FLOOD_NAME_IP = 'one_time_password.ip';
+
+  /**
+   * The window of time flood entries exist for.
+   */
+  const FLOOD_WINDOW = 3600;
+
+  /**
+   * The maximum number of OTP attempts you are allowed.
+   */
+  const FLOOD_THRESHOLD = 5;
+
   /**
    * Implements hook_form_user_login_form_alter().
    */
@@ -46,6 +66,24 @@ class UserLoginEnforce {
       return;
     }
 
+    /** @var \Drupal\Core\Flood\FloodInterface $flood */
+    $flood = \Drupal::service('flood');
+    $ip_address = \Drupal::requestStack()->getCurrentRequest()->getClientIp();
+
+    // Always register a hit against the flood tables.
+    $flood->register(static::FLOOD_NAME_UID, static::FLOOD_WINDOW, $uid);
+    $flood->register(static::FLOOD_NAME_IP, static::FLOOD_WINDOW, $ip_address);
+
+    // Check if the user has attempted too many OTP guesses from a single IP
+    // address or for a specific user.
+    $flood_is_allowed_uid = $flood->isAllowed(static::FLOOD_NAME_UID, static::FLOOD_THRESHOLD, static::FLOOD_WINDOW, $uid);
+    $flood_is_allowed_ip = $flood->isAllowed(static::FLOOD_NAME_IP, static::FLOOD_THRESHOLD, static::FLOOD_WINDOW, $ip_address);
+    if (!$flood_is_allowed_uid || !$flood_is_allowed_ip) {
+      $form_state->setErrorByName('one_time_password', t('There have been too many incorrect one time passwords entered.'));
+      // Don't attempt to validate the OTP if the flood control has kicked in.
+      return;
+    }
+
     // Get the one time password object from the user and verify it against the
     // one time password that was submitted. The flood table validation runs
     // before this validation, so this is not a bruteforce vector. We only see
diff --git a/tests/src/Functional/UserLoginEnforceTest.php b/tests/src/Functional/UserLoginEnforceTest.php
index d500357..8945b88 100644
--- a/tests/src/Functional/UserLoginEnforceTest.php
+++ b/tests/src/Functional/UserLoginEnforceTest.php
@@ -103,7 +103,8 @@ class UserLoginEnforceTest extends BrowserTestBase {
     $tfa_user->one_time_password->regenerateOneTimePassword();
     $tfa_user->save();
 
-    // Login 5 times incorrectly to trigger a flood warning.
+    // Login 6 times incorrectly to trigger a flood warning from core as well
+    // as the OTP threshold.
     foreach (range(0, 5) as $i) {
       $this->drupalPostForm('user/login', [
         'name' => $tfa_user->getUsername(),
@@ -119,7 +120,56 @@ class UserLoginEnforceTest extends BrowserTestBase {
       'one_time_password' => '123',
     ], 'Log in');
     $this->assertSession()->pageTextContains('There have been more than 3 failed login attempts');
+    // Messages from OTP should not indicate if a password was guessed correctly
+    // or not. Verify none of the messages we send are visible.
     $this->assertSession()->pageTextNotContains('The entered two factor authentication code is incorrect.');
+    $this->assertSession()->pageTextNotContains('There have been too many incorrect one time passwords entered.');
+  }
+
+  /**
+   * Test the flood control.
+   */
+  public function testOtpFloodControl() {
+    $tfa_user = $this->drupalCreateUser();
+    $tfa_user->one_time_password->regenerateOneTimePassword();
+    $tfa_user->save();
+
+    $second_tfa_user = $this->drupalCreateUser();
+    $second_tfa_user->one_time_password->regenerateOneTimePassword();
+    $second_tfa_user->save();
+
+    // Login enough times with the correct password to trigger a flood control
+    // warning from OTP.
+    foreach (range(0, 7) as $i) {
+      $this->drupalPostForm('user/login', [
+        'name' => $tfa_user->getUsername(),
+        'pass' => $tfa_user->passRaw,
+        'one_time_password' => '123',
+      ], 'Log in');
+    }
+    $this->assertSession()->pageTextContains('There have been too many incorrect one time passwords entered.');
+
+    // Login with a second user to verify there is a per ip address limit.
+    $this->drupalPostForm('user/login', [
+      'name' => $second_tfa_user->getUsername(),
+      'pass' => $second_tfa_user->passRaw,
+      'one_time_password' => '123',
+    ], 'Log in');
+    $this->assertSession()->pageTextContains('There have been too many incorrect one time passwords entered.');
+
+    // Update all flood ip address identifiers and attempt to login with the
+    // first user to verify there is a per-user limit.
+    \Drupal::database()
+      ->update('flood')
+      ->condition('event', 'one_time_password.ip')
+      ->fields(['identifier' => 'not a real ip'])
+      ->execute();
+    $this->drupalPostForm('user/login', [
+      'name' => $tfa_user->getUsername(),
+      'pass' => $tfa_user->passRaw,
+      'one_time_password' => '123',
+    ], 'Log in');
+    $this->assertSession()->pageTextContains('There have been too many incorrect one time passwords entered.');
   }
 
 }
