Brazil's biggest MU Online portal — since 2003
Tutorial Advanced Infrastructure

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.

GA Gabriel · Updated on Nov 15, 2025 · ⏱ 16 min read
Quick answer

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

LayerWhat it controlsGranularity
Reverse proxy (Nginx/Cloudflare)Raw request volume per IPCoarse, but very cheap on resources
Application (PHP/Node)Business rules: login attempts per account, redemptions per playerFine, context-aware
DatabaseConcurrent connections and slow queriesLast 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/m limits the login zone to 5 requests per minute per IP.
  • burst=2 nodelay allows a small traffic burst without queuing the request, common with duplicate page reloads.
  • limit_req_status 429 returns 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:

EndpointSuggested limitRationale
Panel login5 attempts / 5 min per IP+accountLegitimate use rarely exceeds 2-3 attempts
Promo code redemption10 attempts / 10 min per accountPrevents brute-forcing short codes
Ranking lookup (public API)30 req/min per IPSupports automatic page refresh without opening the door to heavy scraping
Store purchase (WCoin)20 req/min per accountReal transactions rarely exceed this volume per user
New account registration3 registrations / hour per IPLimits 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

SymptomLikely causeFix
Legitimate players blocked at loginLimit calibrated only by IP, ignoring shared NATCombine IP + account in the rate limit key
Rate limiting doesn't reduce brute-force attacksLimit applied only at the application level, no proxy in frontAdd limit_req in Nginx as the first layer
Ranking page slow even with rate limitingDirect database query on every request, no cacheServe the ranking from a cache updated by a scheduled job
Generic error confuses the player429 response with no Retry-After or clear messageInclude the Retry-After header and a specific message in the error JSON
Rate limit counters "disappear" after server restartCounters in local memory instead of RedisUse persistent/shared storage (Redis) for the counters

Rate limiting implementation checklist

  • limit_req configured 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-After and 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.

GA
Guides & builds editor

Gabriel covers gameplay, class builds, PvP and progression. He tests every strategy on a live server before publishing.

Keep reading

Related articles