A simple thread-safe rate limiter implemented in Python for the Eshopbox Backend Engineering Test.
The limiter ensures that no more than a configurable number of requests (N) are allowed during a one-second window, even when multiple threads call the limiter concurrently.
External sales-channel APIs impose limits on how many requests can be made per second.
The rate limiter exposes:
allow_request() -> boolIt returns:
True— the request is allowed.False— the configured limit has already been reached for the current second.
For example, with a limit of 3 requests per second:
Request 1 → True
Request 2 → True
Request 3 → True
Request 4 → False
Request 5 → False
When the next one-second window begins, the counter resets and requests can be allowed again.
The implementation uses a fixed-window counter.
The RateLimiter maintains:
limit— maximum requests allowed per second.request_count— number of requests already allowed in the current window.current_second— identifies the active one-second window.lock— protects shared state when multiple threads access the limiter.
For each call to allow_request():
- Get the current second.
- Acquire the lock.
- If a new second has started, reset the request counter.
- If the counter has reached the configured limit, return
False. - Otherwise, increment the counter and return
True.
Multiple threads may call allow_request() at approximately the same time.
Without synchronization, multiple threads could read the same counter value before another thread updates it, causing more than N requests to be allowed.
The implementation uses Python's threading.Lock to protect the critical section containing the window check, counter reset, limit check, and counter increment.
This ensures that only one thread can modify the shared rate-limiter state at a time.
For example, the concurrency test creates 100 threads against a limiter configured for 10 requests per second.
Expected result:
100 concurrent attempts
10 allowed
90 rejected
eshopbox_rate_limiter/
├── rate_limiter.py # Rate limiter implementation
├── test_rate_limiter.py # Unit and concurrency tests
├── requirements.txt # Python dependencies
├── README.md # Project documentation
└── .gitignore
Contains the RateLimiter class and the allow_request() method.
This is the actual implementation of the rate limiter.
Contains four test functions that verify different behaviors of the same RateLimiter implementation.
Running:
pytest -vshould show:
test_basic_limit PASSED
test_configurable_limit PASSED
test_window_reset PASSED
test_concurrent_requests PASSED
4 passed
4 passed means that all four test functions passed. It does not mean that only four requests were made.
Tests the basic rate-limiting behavior.
The limiter is configured as:
limiter = RateLimiter(3)Four requests are attempted during the same window.
Expected behavior:
Request 1 → True
Request 2 → True
Request 3 → True
Request 4 → False
The first three requests are allowed because the limit is 3. The fourth request is rejected because the limit has already been reached.
Run only this test using:
pytest test_rate_limiter.py::test_basic_limit -vTests that the limit N can be configured instead of being hardcoded.
The current test uses:
limit = 7
number_of_requests = 8This means the limiter allows at most 7 requests while the test attempts 8 requests.
Expected result:
Request 1 → True
Request 2 → True
Request 3 → True
Request 4 → True
Request 5 → True
Request 6 → True
Request 7 → True
Request 8 → False
Allowed: 7
Rejected: 1
The values can be changed to test different limits and request counts.
For example:
limit = 5
number_of_requests = 10would result in 5 allowed and 5 rejected requests during the same window.
To run this test and see the printed request results:
pytest test_rate_limiter.py::test_configurable_limit -s -vThe -s option allows the print() output from the test to be displayed.
Tests that the rate limit resets when a new one-second window begins.
The limiter is configured as:
limiter = RateLimiter(2)During the first second:
Request 1 → True
Request 2 → True
Request 3 → False
The test then waits until the current Unix second changes.
During the next second, a fresh quota is available:
Request 4 → True
Request 5 → True
Request 6 → False
This verifies that the limit applies per second rather than permanently blocking requests after the limit is reached.
Run only this test using:
pytest test_rate_limiter.py::test_window_reset -vTests the main concurrency requirement of the assignment.
The test uses:
limit = 10
number_of_threads = 100This simulates 100 threads sharing the same rate limiter and attempting to call allow_request() at approximately the same time.
Each thread represents one concurrent caller attempting to get permission to make a request.
A threading.Barrier is used to make the worker threads reach allow_request() at approximately the same time.
Expected result:
Total attempts: 100
Allowed: 10
Rejected: 90
The test verifies:
assert len(results) == 100
assert results.count(True) == 10
assert results.count(False) == 90This demonstrates that even when many threads access the limiter concurrently, no more than the configured limit can succeed during the same second.
Run only the concurrency test using:
pytest test_rate_limiter.py::test_concurrent_requests -vCreate a virtual environment:
python -m venv venvActivate it on Windows:
venv\Scripts\activateInstall dependencies:
pip install -r requirements.txtRun all tests:
pytest -vTo also display print() statements:
pytest -s -vExpected result:
test_rate_limiter.py::test_basic_limit PASSED
test_rate_limiter.py::test_configurable_limit PASSED
test_rate_limiter.py::test_window_reset PASSED
test_rate_limiter.py::test_concurrent_requests PASSED
4 passed
The phrase "current second" in the problem statement is interpreted as a fixed one-second window, so this implementation uses a fixed-window counter rather than a sliding-window or token-bucket algorithm.
The limiter is thread-safe for multiple threads sharing the same RateLimiter instance within a single Python process.
For a distributed system with multiple processes or application instances, the rate-limit state would need to be stored in shared infrastructure such as Redis and updated atomically.
Each allow_request() call performs constant-time operations.
- Time: O(1)
- Space: O(1)