Repository: praw-dev/prawcore

Issue:
Rate limiting is overly conservative
Currently the sleep time between requests is calculated based purely on the requests remaining and the seconds to reset, and doesn't take into account the time it takes for the request itself.

As an example, my tests are averaging 0.65 seconds for the request itself to return. So right after the reset, with 600 requests and 600 seconds, praw will wait 1 second, then the request takes 0.65 seconds. If I hit the middle of the window, say 600 requests left and 300 seconds, it will wait 0.5 seconds, then the request takes 0.65 seconds, which still results in a total wait time of 1.15 seconds.

So in 600 seconds you end up sending something like 350-400 requests.

I think this should be changed to have no sleep time at all if the number of requests remaining is greater than the number of seconds until reset. Or possibly a bit of buffer, if the requests remaining is, say, more than 10 above the number of seconds until reset. Just to make sure there's no edge case where you end up waiting 10 seconds for something.

Comments:

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 20b5528..18dffc4 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -13,4 +13,5 @@ Contributors
 - elnuno `@elnuno <https://github.com/elnuno>`_
 - Zeerak Waseem <zeerak.w@gmail.com> `@ZeerakW <https://github.com/ZeerakW>`_
 - jarhill0 `@jarhill0 <https://github.com/jarhill0>`_
+- Watchful1 `@Watchful1 <https://github.com/Watchful1>`_
 - Add "Name <email (optional)> and github profile link" above this line.
diff --git a/CHANGES.rst b/CHANGES.rst
index 2f86871..9a380d0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -7,6 +7,11 @@ that deprecations will not be announced by a minor release.
 Unreleased
 ----------
 
+**Changed**
+
+* Updated rate limit algorithm to more intelligently rate limit when there
+  are extra requests remaining.
+
 **Removed**
 
 * Drop python 2.7 support.
diff --git a/prawcore/rate_limit.py b/prawcore/rate_limit.py
index b01c984..942852d 100644
--- a/prawcore/rate_limit.py
+++ b/prawcore/rate_limit.py
@@ -67,7 +67,6 @@ class RateLimiter(object):
             return
 
         now = time.time()
-        prev_remaining = self.remaining
 
         seconds_to_reset = int(response_headers["x-ratelimit-reset"])
         self.remaining = float(response_headers["x-ratelimit-remaining"])
@@ -78,12 +77,7 @@ class RateLimiter(object):
             self.next_request_timestamp = self.reset_timestamp
             return
 
-        if prev_remaining is not None and prev_remaining > self.remaining:
-            estimated_clients = prev_remaining - self.remaining
-        else:
-            estimated_clients = 1.0
-
         self.next_request_timestamp = min(
             self.reset_timestamp,
-            now + (estimated_clients * seconds_to_reset / self.remaining),
+            now + max(min((seconds_to_reset - self.remaining) / 2, 10), 0),
         )


Test patch:
diff --git a/tests/test_rate_limit.py b/tests/test_rate_limit.py
index 5833a4f..8f73a0c 100644
--- a/tests/test_rate_limit.py
+++ b/tests/test_rate_limit.py
@@ -70,22 +70,22 @@ class RateLimiterTest(unittest.TestCase):
         self.rate_limiter.update(self._headers(60, 100, 60))
         self.assertEqual(60, self.rate_limiter.remaining)
         self.assertEqual(100, self.rate_limiter.used)
-        self.assertEqual(101, self.rate_limiter.next_request_timestamp)
+        self.assertEqual(100, self.rate_limiter.next_request_timestamp)
 
     @patch("time.time")
     def test_update__compute_delay_with_single_client(self, mock_time):
         self.rate_limiter.remaining = 61
         mock_time.return_value = 100
-        self.rate_limiter.update(self._headers(60, 100, 60))
-        self.assertEqual(60, self.rate_limiter.remaining)
+        self.rate_limiter.update(self._headers(50, 100, 60))
+        self.assertEqual(50, self.rate_limiter.remaining)
         self.assertEqual(100, self.rate_limiter.used)
-        self.assertEqual(101, self.rate_limiter.next_request_timestamp)
+        self.assertEqual(105, self.rate_limiter.next_request_timestamp)
 
     @patch("time.time")
     def test_update__compute_delay_with_six_clients(self, mock_time):
         self.rate_limiter.remaining = 66
         mock_time.return_value = 100
-        self.rate_limiter.update(self._headers(60, 100, 60))
+        self.rate_limiter.update(self._headers(60, 100, 72))
         self.assertEqual(60, self.rate_limiter.remaining)
         self.assertEqual(100, self.rate_limiter.used)
         self.assertEqual(106, self.rate_limiter.next_request_timestamp)