Home / Articles / Web Development
Web Development

Open APIs Are Not Free to Abuse: How to Add Rate Limiting to Your Web Application

An API endpoint that works well today can become overwhelmed when called too frequently, whether due to bugs, bots, or abuse. Rate limiting helps restrict the number of requests without immediately increasing capacity...

API Terbuka Bukan Berarti Bebas Dipukul: Cara Menambahkan Rate Limiting di Aplikasi Web

An API may seem fine when used by only a few users. Problems usually arise when a single client sends hundreds of requests in a short period, whether due to repeated button clicks, a misconfigured automated process, or a bot trying to explore the endpoint.

This is where rate limiting is needed. Simply put, rate limiting is a rule that restricts how many times an identity can call an endpoint within a certain period. That identity can be an IP address, user ID, API key, or a combination of several factors.

Rate limiting is not a substitute for authentication and is not a solution for all attacks. However, this mechanism serves as a relatively simple layer of protection to reduce server load, prevent light abuse, and make API behavior more predictable.

Why is request limiting important?

Without limits, a problematic process can exhaust database connections, fill up PHP-FPM workers, or slow down certain endpoints for all users. The impact is not always visible as a major attack. Sometimes the cause is just JavaScript making repeated requests because its retry condition never stops.

Rate limiting also helps protect sensitive endpoints, such as login, sending verification codes, data searches, report generation, and form submissions. Such endpoints are usually more expensive to process or have a higher risk of abuse.

Set limits based on endpoint types

A common mistake is applying a single limit across the entire API. In fact, each endpoint has different costs and risks.

  • Simple read endpoints: for example, a list of categories or public configurations. The limits can be more lenient.
  • Search endpoints: need to be limited because complex queries can burden the database.
  • Login endpoints: require strict limits to reduce repeated password attempts.
  • Email or SMS sending endpoints: usually need to be very strict due to direct costs.
  • Upload or report generation endpoints: need to consider file size and processing time, not just the number of requests.

Start by measuring normal usage patterns. Do not set numbers based solely on guesses. If the majority of users need at most 30 requests per minute, a limit of 60 requests per minute can be a reasonable starting point, then adjusted based on logs.

Choose the identity to limit

Limiting by IP is the easiest method, but it is not always fair. Many users can be behind a single office, campus, or mobile operator network. If the limit is too low, everyone on that network can be affected.

For APIs that already have authentication, user ID or API key is usually more appropriate. You can also combine several rules: limit by IP for anonymous users and limit by account for logged-in users.

Keep in mind that the IP address received by the application can be affected by proxies or load balancers. Do not blindly trust headers like X-Forwarded-For if your infrastructure is not configured to recognize trusted proxies. Misreading that header can cause many users to be considered as coming from the same IP or vice versa.

Simple implementation with PHP and MySQL

For small applications, rate limiting can start from a simple table. For example, store the number of requests and the time window for each identity.

CREATE TABLE api_rate_limits (
    identity_key VARCHAR(190) PRIMARY KEY,
    window_started_at DATETIME NOT NULL,
    request_count INT NOT NULL DEFAULT 0,
    updated_at DATETIME NOT NULL
);

The basic logic is as follows: retrieve the record for that identity, check if the time window is still valid, then increment the request count. If the window has expired, reset the count to zero.

$limit = 60;
$windowSeconds = 60;
$key = 'user:' . $userId;

// Retrieve data based on $key with a parameterized query.
// Check the time window and request count.
// If still within the window and count >= $limit, send HTTP 429.
// If not exceeded, increment count atomically. 'too_many_requests',
    'message' => 'Too many requests. Please try again later.'
]);

This example intentionally does not include the entire production code because transaction and concurrency details are very important. Two simultaneous requests can read the same count value and write incorrect results if the process is not done atomically.

Use transactions, upsert operations, or storage mechanisms designed for fast counters. Ensure the query has an index on identity_key and test behavior when many requests come in simultaneously.

Redis is often better suited for temporary counters

Rate limit data is usually temporary. Therefore, Redis or similar in-memory storage is often more appropriate than writing every request to the main table. With operations like INCR and expiration, counters can be made lighter.

However, Redis is not free from issues. You still need to consider what happens if Redis is unavailable. For regular endpoints, the application may use a looser backup limit. For sensitive endpoints like login or OTP sending, the failure of rate limit storage needs to be handled more carefully.

Don't forget to provide a useful response

When the limit is exceeded, use HTTP status 429 Too Many Requests. Include a clear message and, if possible, information on when the client can try again.

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

The Retry-After header helps clients create a more orderly retry strategy. Without that information, clients might immediately try again in the same pattern, prolonging the queue.

On the client side, avoid unlimited retries. Use exponential backoff, so the intervals between attempts get longer. For unsafe operations that are retried, such as placing orders or payments, rate limiting should be complemented with idempotency to prevent duplicate transactions.

What does this mean for us?

Rate limiting is not just about adding a maximum number to the API. It is a way to express that the system's capacity has limits and that each type of request needs to be treated according to its cost.

What you can do now:

  1. Log the number of requests per endpoint, identity, response status, and processing time.
  2. Select the most expensive or sensitive endpoints to limit first.
  3. Determine whether the limits will use IP, user ID, API key, or a combination of identities.
  4. Return status 429 along with a clear message and retry time.
  5. Test simultaneous requests to ensure the counter is not easily breached by race conditions.
  6. Review limits after observing real usage data, not just estimates.

A good limit is not felt by normal users but kicks in when usage patterns become abnormal. With a gradual approach and careful monitoring, rate limiting can keep the API responsive without making the application feel rigid for legitimate users.

– Rio Yotto @rioyotto