Rate Limiting on Your MU Online Server APIs: How to Prevent Abuse and Attacks
Implement rate limiting on your MU Online server's panel and website APIs, protecting login, ranking, and the store against brute-force, abusive scraping, and database overload.
Every API exposed by an MU Online server's website or panel — login, ranking lookup, promo code redemption, WCoin store purchases — is a potential target for abuse: bots trying to guess passwords, scrapers hitting the ranking hundreds of times per second, or simply a spike in legitimate traffic that
Every API exposed by an MU Online server's website or panel — login, ranking lookup, promo code redemption, WCoin store purchases — is a potential target for abuse: bots trying to guess passwords, scrapers hitting the ranking hundreds of times per second, or simply a spike in legitimate traffic that takes down the database. Rate limiting is the technique of capping how many requests a source can make within a time window, and it's one of the most cost-effective infrastructure defenses out there: a few lines of configuration prevent hours of instability and a good fraction of intrusion attempts. This tutorial covers implementing it in two layers — reverse proxy (Nginx) and application (PHP/Node with Redis) — applied to the most sensitive endpoints of an MU server.
Why MU server APIs are a constant target
Unlike a regular institutional website, an MU server's site has a direct economic incentive behind several endpoints: the ranking influences reputation and attracts players, login guards accounts with valuable items, and the store moves real money via WCoin. This attracts three recurring types of abuse: login brute-force (mass password testing), ranking scraping (bots capturing data for third-party sites or overloading the database), and code redemption abuse (scripts trying to redeem promo coupons in volume before they expire).
Layers where rate limiting should be applied
| Layer | What it controls | Granularity |
|---|---|---|
| Reverse proxy (Nginx/Cloudflare) | Raw request volume per IP | Coarse, but very cheap on resources |
| Application (PHP/Node) | Business rules: login attempts per account, redemptions per player | Fine, context-aware |
| Database | Concurrent connections and slow queries | Last line of defense, already in emergency mode |
No single layer is enough on its own. The proxy catches raw volume from simple bots; the application catches more sophisticated abuse that spreads requests across few IPs but concentrates on a single account.
Configuring rate limiting in Nginx
Nginx's limit_req module is the most efficient way to block volume before it ever reaches PHP-FPM or Node.
# Inside the http {} block
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=ranking:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=geral:10m rate=60r/m;
server {
location /painel/login.php {
limit_req zone=login burst=2 nodelay;
limit_req_status 429;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
location /api/ranking {
limit_req zone=ranking burst=10 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
location / {
limit_req zone=geral burst=20 nodelay;
}
}
rate=5r/mlimits the login zone to 5 requests per minute per IP.burst=2 nodelayallows a small traffic burst without queuing the request, common with duplicate page reloads.limit_req_status 429returns the correct HTTP code ("Too Many Requests") instead of the default 503, making it easier to handle on the front end.
Rate limiting in the application with Redis
For rules that depend on business context (a specific account, not just an IP), implement the control in the application using Redis as shared storage for the counters.
<?php
function verificarRateLimit(Redis $redis, string $chave, int $limite, int $janelaSegundos): bool {
$atual = $redis->incr($chave);
if ($atual === 1) {
$redis->expire($chave, $janelaSegundos);
}
return $atual <= $limite;
}
// Usage in the login endpoint
$chave = 'login_tentativas:' . $_SERVER['REMOTE_ADDR'] . ':' . $_POST['usuario'];
if (!verificarRateLimit($redis, $chave, 5, 300)) {
http_response_code(429);
die(json_encode(['erro' => 'Muitas tentativas. Tente novamente em alguns minutos.']));
}
This approach combines IP and username in the key, which avoids unfairly blocking an entire network (mobile carrier NAT, for example) while still preventing a single bot from mass-testing passwords against a specific account.
Setting limits per endpoint
Each endpoint has a different legitimate usage profile, and the limit should reflect that:
| Endpoint | Suggested limit | Rationale |
|---|---|---|
| Panel login | 5 attempts / 5 min per IP+account | Legitimate use rarely exceeds 2-3 attempts |
| Promo code redemption | 10 attempts / 10 min per account | Prevents brute-forcing short codes |
| Ranking lookup (public API) | 30 req/min per IP | Supports automatic page refresh without opening the door to heavy scraping |
| Store purchase (WCoin) | 20 req/min per account | Real transactions rarely exceed this volume per user |
| New account registration | 3 registrations / hour per IP | Limits mass account creation for farming/bots |
Responding correctly when the limit is exceeded
Returning just a generic error frustrates the legitimate player who simply had bad luck hitting the cap. The ideal response includes the Retry-After header and a clear message:
<?php
http_response_code(429);
header('Retry-After: 60');
echo json_encode([
'erro' => 'rate_limit_excedido',
'mensagem' => 'Você atingiu o limite de tentativas. Tente novamente em 60 segundos.'
]);
On the front end, catch the 429 status and display a countdown instead of letting the user keep hammering the button, which would only prolong the block.
Rate limiting specific to ranking scraping
Third-party ranking sites (MU server aggregators) often scrape your ranking at high frequency to keep their own data up to date. If this overloads your database, consider: (1) publishing an official API endpoint with a few minutes of caching, served by a scheduled job instead of a direct database query on every request, and (2) applying a more generous, but real, limit to that public endpoint, making it clear in the site's terms that scraping outside this API is prohibited.
Monitoring and fine-tuning
Poorly calibrated rate limiting on day one is normal — the goal is to log and adjust. Keep a log of rejections (429) with IP, endpoint, and timestamp, and review it weekly during the first weeks after launch:
# Example of counting rejections per endpoint in Nginx logs
grep ' 429 ' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn
If a specific endpoint generates many rejections from distinct IPs with no apparent abuse pattern, the limit is probably calibrated below real legitimate usage — raise it. If most rejections come from a few repeated IPs, the limit is working as expected.
Rate limiting versus CAPTCHA and other complementary defenses
Rate limiting doesn't replace CAPTCHA on registration and password recovery forms — it reduces the volume of attempts, but doesn't distinguish human from bot within the allowed limit. For the highest-risk endpoints (account registration, password recovery), combine rate limiting with CAPTCHA (reCAPTCHA or hCaptcha) and, when it makes sense, email verification before fully activating a new account.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Legitimate players blocked at login | Limit calibrated only by IP, ignoring shared NAT | Combine IP + account in the rate limit key |
| Rate limiting doesn't reduce brute-force attacks | Limit applied only at the application level, no proxy in front | Add limit_req in Nginx as the first layer |
| Ranking page slow even with rate limiting | Direct database query on every request, no cache | Serve the ranking from a cache updated by a scheduled job |
| Generic error confuses the player | 429 response with no Retry-After or clear message | Include the Retry-After header and a specific message in the error JSON |
| Rate limit counters "disappear" after server restart | Counters in local memory instead of Redis | Use persistent/shared storage (Redis) for the counters |
Rate limiting implementation checklist
limit_reqconfigured in Nginx for login, ranking, and store endpoints.- Business rules (IP + account) implemented in the application via Redis or equivalent.
- Per-endpoint limits defined based on expected legitimate volume.
- 429 response with
Retry-Afterand a clear message implemented. - CAPTCHA combined with registration and password recovery.
- Ranking endpoint with scheduled caching, reducing database load.
- Rejection logs monitored and limits adjusted during the first weeks.
With rate limiting live, the natural next step is to review the other security layers of the panel's APIs — input validation, CSRF protection, and authentication — so the server has real defense in depth. If the infrastructure is still in the planning stage, it's worth reviewing the MU Online server creation guide to align these practices from the foundation up. </content>
Frequently asked questions
Is rate limiting the same thing as a firewall?
No. A firewall blocks based on network rules (IP, port, protocol); rate limiting controls the frequency of legitimate requests to a specific endpoint, regardless of whether the origin is 'trusted' or not. The two complement each other.
Where should I apply rate limiting: Nginx, the application, or both?
Ideally both. Nginx (or an equivalent reverse proxy) filters raw volume before it reaches the application, saving resources; the application applies finer rules, like a limit per user account rather than just per IP, which the proxy alone can't see.
Can rate limiting block legitimate players?
It can, if configured too aggressively or without accounting for shared IPs (mobile networks, provider NAT). That's why it's important to limit by IP + account combination when possible, and always return a clear message instead of simply locking the page.
Do I need Redis to implement rate limiting?
It's not mandatory, but it's the most robust option when you have more than one web server behind a load balancer, because the request counter needs to be shared across instances. For a single server, file-based or in-application-memory caching already covers most cases.
What's a reasonable request limit for the login page?
A common reference is 5 attempts per IP every 5 minutes for login, with progressive backoff on subsequent attempts. Adjust based on your server's actual player volume and monitor false positives in the first weeks.