diff --git a/cms/envs/bok_choy.yml b/cms/envs/bok_choy.yml
index c6c90bddf88d9b203847bfcd3a63b4cf73e840f8..6aa54f0b849b4ef30696db1d799c8aa3c7ef7ebe 100644
--- a/cms/envs/bok_choy.yml
+++ b/cms/envs/bok_choy.yml
@@ -112,6 +112,9 @@ MODULESTORE:
       - ENGINE: xmodule.modulestore.xml.XMLModuleStore
         NAME: xml
         OPTIONS: {data_dir: '** OVERRIDDEN **', default_class: xmodule.hidden_module.HiddenDescriptor}
+# We need to test different scenarios, following setting effectively disbale rate limiting
+PASSWORD_RESET_IP_RATE: '1/s'
+PASSWORD_RESET_EMAIL_RATE: '1/s'
 SECRET_KEY: ''
 SERVER_EMAIL: devops@example.com
 SESSION_COOKIE_DOMAIN: null
diff --git a/cms/envs/common.py b/cms/envs/common.py
index edbfb8b849b13abbe0480dd973d78694a38da1a3..11dc5dd9cc22c4566fe1529840c82f198264289e 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -2254,3 +2254,7 @@ DISABLE_DEPRECATED_SIGNUP_URL = False
 
 ##### LOGISTRATION RATE LIMIT SETTINGS #####
 LOGISTRATION_RATELIMIT_RATE = '100/5m'
+
+##### PASSWORD RESET RATE LIMIT SETTINGS #####
+PASSWORD_RESET_IP_RATE = '1/m'
+PASSWORD_RESET_EMAIL_RATE = '2/h'
diff --git a/common/djangoapps/util/request_rate_limiter.py b/common/djangoapps/util/request_rate_limiter.py
index 384ac28276935d6742c62a4b594484e42bc57383..015890963d815c37385b33a541e6779f37c31605 100644
--- a/common/djangoapps/util/request_rate_limiter.py
+++ b/common/djangoapps/util/request_rate_limiter.py
@@ -3,9 +3,6 @@ A utility class which wraps the RateLimitMixin 3rd party class to do bad request
 which can be used for rate limiting
 """
 
-from datetime import datetime, timedelta
-
-from django.conf import settings
 from ratelimitbackend.backends import RateLimitMixin
 
 
@@ -32,72 +29,3 @@ class BadRequestRateLimiter(RequestRateLimiter):
     Default rate limit is 30 requests for every 5 minutes.
     """
     pass
-
-
-class PasswordResetEmailRateLimiter(RequestRateLimiter):
-    """
-    Rate limiting requests to send password reset emails.
-    """
-    email_rate_limit = getattr(settings, 'PASSWORD_RESET_EMAIL_RATE_LIMIT', {})
-    requests = email_rate_limit.get('no_of_emails', 1)
-    cache_timeout_seconds = email_rate_limit.get('per_seconds', 60)
-    reset_email_cache_prefix = 'resetemail'
-
-    def key(self, request, dt):
-        """
-        Returns IP based cache key.
-        """
-        return '%s-%s-%s' % (
-            self.reset_email_cache_prefix,
-            self.get_ip(request),
-            dt.strftime('%Y%m%d%H%M'),
-        )
-
-    def email_key(self, request, dt):
-        """
-        Returns email based cache key.
-        """
-        return '%s-%s-%s' % (
-            self.reset_email_cache_prefix,
-            self.get_email(request),
-            dt.strftime('%Y%m%d%H%M'),
-        )
-
-    def expire_after(self):
-        """
-        Returns timeout for cache keys.
-        """
-        return self.cache_timeout_seconds
-
-    def get_email(self, request):
-        """
-        Returns email id for cache key.
-        """
-        user = request.user
-        # Prefer logged-in user's email
-        email = user.email if user.is_authenticated else request.POST.get('email')
-        return email
-
-    def keys_to_check(self, request):
-        """
-        Return list of IP and email based keys.
-        """
-        keys = super(PasswordResetEmailRateLimiter, self).keys_to_check(request)
-
-        now = datetime.now()
-        email_keys = [
-            self.email_key(
-                request,
-                now - timedelta(minutes=minute),
-            ) for minute in range(self.minutes + 1)
-        ]
-        keys.extend(email_keys)
-
-        return keys
-
-    def tick_request_counter(self, request):
-        """
-        Ticks any counters used to compute when rate limit has been reached.
-        """
-        for key in self.keys_to_check(request):
-            self.cache_incr(key)
diff --git a/lms/envs/bok_choy.yml b/lms/envs/bok_choy.yml
index 9e555ebaaac9547eb7f64668f2e8094a49ccc707..6feab78ef8cecc42e48b9dd80ff17c05ee13babb 100644
--- a/lms/envs/bok_choy.yml
+++ b/lms/envs/bok_choy.yml
@@ -211,9 +211,8 @@ MODULESTORE:
         NAME: xml
         OPTIONS: {data_dir: '** OVERRIDDEN **', default_class: xmodule.hidden_module.HiddenDescriptor}
 # We need to test different scenarios, following setting effectively disbale rate limiting
-PASSWORD_RESET_EMAIL_RATE_LIMIT:
-  no_of_emails: 1
-  per_seconds: 1
+PASSWORD_RESET_IP_RATE: '1/s'
+PASSWORD_RESET_EMAIL_RATE: '1/s'
 PASSWORD_RESET_SUPPORT_LINK: https://support.example.com/password-reset-help.html
 REGISTRATION_EXTENSION_FORM: openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm
 REGISTRATION_EXTRA_FIELDS: {city: hidden, country: required, gender: optional, goals: optional,
@@ -237,7 +236,7 @@ TECH_SUPPORT_EMAIL: technical@example.com
 THIRD_PARTY_AUTH_BACKENDS: [social_core.backends.google.GoogleOAuth2, social_core.backends.linkedin.LinkedinOAuth2,
   social_core.backends.facebook.FacebookOAuth2, third_party_auth.dummy.DummyBackend,
   third_party_auth.saml.SAMLAuthBackend]
-THIRD_PARTY_AUTH: 
+THIRD_PARTY_AUTH:
   Google:
     SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test"
     SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test"
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 38221127d86d1f35a78d3e7dac756fe40b1916c6..6792b1398f6f9fbc07de8d28513c00d05fb1e9c4 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -531,10 +531,6 @@ SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60
 # Maximum of 6 retries before giving up.
 SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6
 
-PASSWORD_RESET_EMAIL_RATE_LIMIT = {
-    'no_of_emails': 1,
-    'per_seconds': 60
-}
 RETRY_CALENDAR_SYNC_EMAIL_MAX_ATTEMPTS = 5
 # Deadline message configurations
 COURSE_MESSAGE_ALERT_DURATION_IN_DAYS = 14
@@ -3777,6 +3773,10 @@ RATELIMIT_RATE = '120/m'
 ##### LOGISTRATION RATE LIMIT SETTINGS #####
 LOGISTRATION_RATELIMIT_RATE = '100/5m'
 
+##### PASSWORD RESET RATE LIMIT SETTINGS #####
+PASSWORD_RESET_IP_RATE = '1/m'
+PASSWORD_RESET_EMAIL_RATE = '2/h'
+
 ############### Settings for Retirement #####################
 RETIRED_USERNAME_PREFIX = 'retired__user_'
 RETIRED_EMAIL_PREFIX = 'retired__user_'
diff --git a/openedx/core/djangoapps/user_authn/views/password_reset.py b/openedx/core/djangoapps/user_authn/views/password_reset.py
index 066ee2f47bda70ebc573cdb336f27c5a250a0ef8..17d5276b8aee1e7115c4b4498a162d6108f46ac4 100644
--- a/openedx/core/djangoapps/user_authn/views/password_reset.py
+++ b/openedx/core/djangoapps/user_authn/views/password_reset.py
@@ -24,6 +24,7 @@ from django.views.decorators.http import require_POST
 from edx_ace import ace
 from edx_ace.recipient import Recipient
 from eventtracking import tracker
+from ratelimit.decorators import ratelimit
 from rest_framework.views import APIView
 
 from edxmako.shortcuts import render_to_string
@@ -43,8 +44,9 @@ from student.forms import send_account_recovery_email_for_user
 from student.models import AccountRecovery
 from util.json_request import JsonResponse
 from util.password_policy_validators import normalize_password, validate_password
-from util.request_rate_limiter import PasswordResetEmailRateLimiter
 
+POST_EMAIL_KEY = 'post:email'
+REAL_IP_KEY = 'openedx.core.djangoapps.util.ratelimit.real_ip'
 SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
 
 # Maintaining this naming for backwards compatibility.
@@ -238,15 +240,19 @@ def request_password_change(email, is_secure):
 
 @csrf_exempt
 @require_POST
+@ratelimit(key=POST_EMAIL_KEY, rate=settings.PASSWORD_RESET_EMAIL_RATE)
+@ratelimit(key=REAL_IP_KEY, rate=settings.PASSWORD_RESET_IP_RATE)
 def password_reset(request):
     """
     Attempts to send a password reset e-mail.
     """
+    user = request.user
+    # Prefer logged-in user's email
+    email = user.email if user.is_authenticated else request.POST.get('email')
+    AUDIT_LOG.info("Password reset initiated for email %s.", email)
 
-    password_reset_email_limiter = PasswordResetEmailRateLimiter()
-
-    if password_reset_email_limiter.is_rate_limit_exceeded(request):
-        AUDIT_LOG.warning("Password reset rate limit exceeded")
+    if getattr(request, 'limited', False):
+        AUDIT_LOG.warning("Password reset rate limit exceeded for email %s.", email)
         return JsonResponse(
             {
                 'success': False,
@@ -277,8 +283,6 @@ def password_reset(request):
         # bad user? tick the rate limiter counter
         AUDIT_LOG.info("Bad password_reset user passed in.")
 
-    password_reset_email_limiter.tick_request_counter(request)
-
     return JsonResponse({
         'success': True,
         'value': render_to_string('registration/password_reset_done.html', {}),
@@ -522,6 +526,8 @@ def _get_user_from_email(email):
 
 
 @require_POST
+@ratelimit(key=POST_EMAIL_KEY, rate=settings.PASSWORD_RESET_EMAIL_RATE)
+@ratelimit(key=REAL_IP_KEY, rate=settings.PASSWORD_RESET_IP_RATE)
 def password_change_request_handler(request):
     """Handle password change requests originating from the account page.
 
@@ -546,20 +552,18 @@ def password_change_request_handler(request):
         POST /account/password
 
     """
+    user = request.user
+    # Prefer logged-in user's email
+    email = user.email if user.is_authenticated else request.POST.get('email')
+    AUDIT_LOG.info("Password reset initiated for user %s.", email)
 
-    password_reset_email_limiter = PasswordResetEmailRateLimiter()
-
-    if password_reset_email_limiter.is_rate_limit_exceeded(request):
-        AUDIT_LOG.warning("Password reset rate limit exceeded")
+    if getattr(request, 'limited', False):
+        AUDIT_LOG.warning("Password reset rate limit exceeded for email %s.", email)
         return HttpResponse(
             _("Your previous request is in progress, please try again in a few moments."),
             status=403
         )
 
-    user = request.user
-    # Prefer logged-in user's email
-    email = user.email if user.is_authenticated else request.POST.get('email')
-
     if email:
         try:
             request_password_change(email, request.is_secure())
@@ -591,7 +595,6 @@ def password_change_request_handler(request):
                           .format(email=email, error=err))
             return HttpResponse(_("Some error occured during password change. Please try again"), status=500)
 
-        password_reset_email_limiter.tick_request_counter(request)
         return HttpResponse(status=200)
     else:
         return HttpResponseBadRequest(_("No email address provided."))
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_password.py b/openedx/core/djangoapps/user_authn/views/tests/test_password.py
index 5bee8f52691b5e76b9e1473c7cbc2bc57fd33d39..f1c9666dc6a3982ae25d0ea6105582efb43f0b3e 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_password.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_password.py
@@ -5,17 +5,21 @@ Tests for user authorization password-related functionality.
 import json
 import logging
 import re
+from datetime import datetime, timedelta
 
 import ddt
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.core import mail
+from django.core.cache import cache
 from django.test import TestCase
 from django.test.client import RequestFactory
 from django.urls import reverse
+from freezegun import freeze_time
 from mock import Mock, patch
 from oauth2_provider.models import AccessToken as dot_access_token
 from oauth2_provider.models import RefreshToken as dot_refresh_token
+from pytz import UTC
 from testfixtures import LogCapture
 
 from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
@@ -110,6 +114,7 @@ class TestPasswordChange(CreateAccountMixin, CacheIsolationTestCase):
         result = self.client.login(username=self.USERNAME, password=self.OLD_PASSWORD)
         self.assertTrue(result)
         mail.outbox = []
+        cache.clear()
 
     def test_password_change(self):
         # Request a password change while logged in, simulating
@@ -259,11 +264,16 @@ class TestPasswordChange(CreateAccountMixin, CacheIsolationTestCase):
             # Send the view an email address not tied to any user
             response = self._change_password(email=self.NEW_EMAIL)
             self.assertEqual(response.status_code, 200)
-            logger.check((LOGGER_NAME, 'INFO', 'Invalid password reset attempt'))
+
+            expected_logs = (
+                (LOGGER_NAME, 'INFO', 'Password reset initiated for user {}.'.format(self.NEW_EMAIL)),
+                (LOGGER_NAME, 'INFO', 'Invalid password reset attempt')
+            )
+            logger.check(*expected_logs)
 
     def test_password_change_rate_limited(self):
         """
-        Tests that consecutive password reset requests are rate limited.
+        Tests that password reset requests are rate limited as expected.
         """
         # Log out the user created during test setup, to prevent the view from
         # selecting the logged-in user's email address over the email provided
@@ -273,11 +283,11 @@ class TestPasswordChange(CreateAccountMixin, CacheIsolationTestCase):
             response = self._change_password(email=self.NEW_EMAIL)
             self.assertEqual(response.status_code, status)
 
-        with patch(
-            'util.request_rate_limiter.PasswordResetEmailRateLimiter.is_rate_limit_exceeded',
-            return_value=False
-        ):
-            response = self._change_password(email=self.NEW_EMAIL)
+        # now reset the time to 1 min from now in future and change the email and
+        # verify that it will allow another request from same IP
+        reset_time = datetime.now(UTC) + timedelta(seconds=61)
+        with freeze_time(reset_time):
+            response = self._change_password(email=self.OLD_EMAIL)
             self.assertEqual(response.status_code, 200)
 
     @ddt.data(
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py
index 8450540a8b6830df5122457d20593a091c5ab154..1a83bc0d93a7406c8755887149c32972b111c88d 100644
--- a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py
@@ -7,6 +7,7 @@ import json
 import re
 import unicodedata
 import unittest
+from datetime import datetime, timedelta
 
 import ddt
 from django.conf import settings
@@ -22,8 +23,10 @@ from django.test.client import RequestFactory
 from django.test.utils import override_settings
 from django.urls import reverse
 from django.utils.http import int_to_base36
+from freezegun import freeze_time
 from mock import Mock, patch
 from oauth2_provider import models as dot_models
+from pytz import UTC
 from six.moves import range
 
 from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
@@ -181,31 +184,41 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
 
     def test_ratelimited_from_different_ips_with_same_email(self):
         """
-        Test that password reset endpoint allow only one request per minute
+        Test that password reset endpoint allow only two requests per hour
         per email address.
         """
         cache.clear()
-        good_req = self.request_factory.post('/password_reset/', {'email': 'thisdoesnotexist@foo.com'})
-        good_req.user = AnonymousUser()
-        good_resp = password_reset(good_req)
-        self.assertEqual(good_resp.status_code, 200)
-
-        # change the IP and verify that the rate limiter should kick in and
-        # give a Forbidden response if the request is for same email address.
+        self.request_password_reset(200)
+        # now reset the time to 1 min from now in future and change the email and
+        # verify that it will allow another request from same IP
+        for status in [200, 403]:
+            reset_time = datetime.now(UTC) + timedelta(seconds=61)
+            with freeze_time(reset_time):
+                self.request_password_reset(status)
+
+        # Even changing the IP will not allow more than two requests for same email.
         new_ip = "8.8.8.8"
-        self.assertNotEqual(good_req.META.get('REMOTE_ADDR'), new_ip)
+        self.request_password_reset(403, new_ip=new_ip)
+
+        cache.clear()
 
-        bad_req = self.request_factory.post(
+    def request_password_reset(self, status, new_ip=None):
+        extra_args = {}
+        if new_ip:
+            extra_args = {'REMOTE_ADDR': new_ip}
+
+        reset_request = self.request_factory.post(
             '/password_reset/',
             {'email': 'thisdoesnotexist@foo.com'},
-            REMOTE_ADDR=new_ip
+            **extra_args
         )
-        bad_req.user = AnonymousUser()
-        bad_resp = password_reset(bad_req)
-        self.assertEqual(bad_resp.status_code, 403)
-        self.assertEqual(bad_req.META.get('REMOTE_ADDR'), new_ip)
 
-        cache.clear()
+        if new_ip:
+            self.assertEqual(reset_request.META.get('REMOTE_ADDR'), new_ip)
+
+        reset_request.user = AnonymousUser()
+        response = password_reset(reset_request)
+        self.assertEqual(response.status_code, status)
 
     @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS")
     @ddt.data(('plain_text', "You're receiving this e-mail because you requested a password reset"),