How to create a server status page (online/offline) for MU
Learn how to build a status page that shows in real time whether your MU Online ConnectServer, GameServer and database are online, with a player count and uptime history.
A status page is one of the elements that most conveys trust in a MU Online server. Before downloading the client, the player wants to know whether the server is up, how many people are playing and whether there have been recent outages. In this tutorial you will build a complete page in PHP that te
A status page is one of the elements that most conveys trust in a MU Online server. Before downloading the client, the player wants to know whether the server is up, how many people are playing and whether there have been recent outages. In this tutorial you will build a complete page in PHP that tests the ConnectServer and GameServer ports over TCP, queries the database to count online players and displays it all in a panel with auto-refresh and uptime history. All the code is commented and the concrete values (ports, table names, fields) appear as examples, since they vary by version.
Prerequisites
Before you start, have the environment set up. If you don't have the server running yet, first see how to create a MU Online server and then come back to this web part.
| Requirement | Detail (example - varies by version) |
|---|---|
| Web server | Apache or Nginx with PHP 7.4+ (ideally 8.1+) |
| Database extension | sqlsrv (SQL Server) or mysqli depending on your distro |
| Port access | ConnectServer (e.g. 44405) and GameServer (e.g. 55901) reachable |
| Database credentials | User with read permission on the MU database |
| Firewall | Check ports opened from the site host to the game host |
The critical point is network access. If the site is on the same VPS as the server, you test 127.0.0.1. If it is on a separate host, you need to open the ports on the game server's firewall to the site's IP. Never leave the administrative ports fully open to the internet just for the sake of the status page - restrict them by source IP.
How status detection works
There are three layers worth monitoring, and each answers a different question:
- Is the ConnectServer online? - it is the server the client contacts first to list the subservers. If it goes down, no one gets in. We test it by opening a TCP socket on its port.
- Is the GameServer online? - it is where the game actually runs. There may be more than one (subservers). We test each one's port.
- Is the database responding? - if SQL Server or MySQL goes down, login fails even with the servers up. We test with a trivial query.
The main technique is the TCP port test: we try to open a connection. If it opens, the service is listening (online). If it is refused or times out, it is offline. This does not validate the MU protocol itself, but in practice it is an excellent and cheap indicator.
Step 1 - Function that tests a TCP port
Create a file status/checker.php. The core function uses fsockopen with a short timeout so it never freezes the page:
<?php
// status/checker.php
/**
* Tests whether a TCP port is accepting connections.
* Returns true (online) or false (offline).
*/
function portaOnline(string $host, int $porta, float $timeout = 2.0): bool {
$errno = 0;
$errstr = '';
// @ suppresses the warning when the connection is refused - we handle the return.
$conn = @fsockopen($host, $porta, $errno, $errstr, $timeout);
if ($conn === false) {
return false; // refused, filtered or timeout = offline
}
fclose($conn);
return true;
}
/**
* Version with latency measurement (ms) - useful for showing a "ping".
*/
function checarServico(string $host, int $porta, float $timeout = 2.0): array {
$inicio = microtime(true);
$online = portaOnline($host, $porta, $timeout);
$latencia = (int) round((microtime(true) - $inicio) * 1000);
return [
'online' => $online,
'latencia' => $online ? $latencia : null,
];
}
The timeout parameter is what stops the page from freezing. With 2 seconds, even if three services are offline, the worst case is 6 seconds - and with caching that becomes almost instant.
Step 2 - Define the monitored services
Centralize the service configuration in an array. That way, when you add a new subserver, you edit only one place:
<?php
// status/config.php
// WARNING: the ports and IPs below are EXAMPLES - they vary by version and setup.
return [
'servicos' => [
'connect' => [
'nome' => 'Connect Server',
'host' => '127.0.0.1',
'porta' => 44405, // check it in ConnectServer.ini
],
'game_1' => [
'nome' => 'Game Server (Sub 1)',
'host' => '127.0.0.1',
'porta' => 55901, // check it in GameServer.ini
],
'game_2' => [
'nome' => 'Game Server (Sub 2)',
'host' => '127.0.0.1',
'porta' => 55902,
],
],
'cache_ttl' => 20, // seconds of status cache
];
Step 3 - Count online players in the database
The port test tells whether the service is alive, but the player wants to see how many people are playing. That comes from the database. In common MU databases (Season 6 and derivatives), the character or account table has a connection-status field. The name varies - it can be ConnectStat, OnlineStatus or a separate MEMB_STAT table - so adjust it to your schema.
<?php
// status/jogadores.php
require_once __DIR__ . '/db.php'; // your connection function (getMuDB)
/**
* Counts online players by reading the connection-status field.
* The field/table name VARIES BY VERSION - adjust to your schema.
*/
function contarOnline(): ?int {
try {
$conn = getMuDB();
} catch (\Throwable $e) {
return null; // database offline
}
// SQL Server example (Character.ConnectStat = 1 means online)
$sql = "SELECT COUNT(*) AS total FROM Character WHERE ConnectStat = 1";
$stmt = sqlsrv_query($conn, $sql);
if ($stmt === false) {
return null;
}
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
return (int) ($row['total'] ?? 0);
}
In some setups, the count by ConnectStat can get "stuck" after a GameServer crash (characters marked as online that actually dropped). If that happens on your server, prefer to read from a status table updated by the GameServer itself, or reset the field at server startup. Document which method you used.
Here is the equivalent SQL for those using MySQL/MariaDB, just changing the syntax:
-- MySQL / MariaDB: count online characters
-- Example field - confirm the name in your database
SELECT COUNT(*) AS total
FROM Character
WHERE ConnectStat = 1;
-- Alternative: total accounts with an active session in a status table
SELECT COUNT(*) AS total
FROM MEMB_STAT
WHERE ConnectStat = 1;
Step 4 - Build the snapshot with caching
Checking the ports and the database on every visit is wasteful and can overload the GameServer if the page goes viral. Store the result in a cache file with a short TTL:
<?php
// status/snapshot.php
require_once __DIR__ . '/checker.php';
require_once __DIR__ . '/jogadores.php';
function gerarSnapshot(): array {
$cfg = require __DIR__ . '/config.php';
$servicos = [];
$algumGameOnline = false;
foreach ($cfg['servicos'] as $chave => $svc) {
$res = checarServico($svc['host'], $svc['porta']);
$servicos[$chave] = [
'nome' => $svc['nome'],
'online' => $res['online'],
'latencia' => $res['latencia'],
];
if ($res['online'] && str_starts_with($chave, 'game')) {
$algumGameOnline = true;
}
}
return [
'atualizado' => date('c'),
'servicos' => $servicos,
// Only count players if at least one GameServer responded.
'online' => $algumGameOnline ? (contarOnline() ?? 0) : 0,
'db_ok' => contarOnline() !== null,
];
}
/**
* Returns the snapshot from cache, or generates a new one if it expired.
*/
function getStatus(): array {
$cfg = require __DIR__ . '/config.php';
$cacheFile = sys_get_temp_dir() . '/mu_status.json';
$ttl = $cfg['cache_ttl'];
if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
$dados = json_decode(file_get_contents($cacheFile), true);
if (is_array($dados)) {
return $dados;
}
}
$snapshot = gerarSnapshot();
file_put_contents($cacheFile, json_encode($snapshot));
return $snapshot;
}
Step 5 - JSON endpoint for the front-end
So the panel can update without reloading the whole page, expose the status as JSON:
<?php
// status/api.php
require_once __DIR__ . '/snapshot.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(getStatus(), JSON_UNESCAPED_UNICODE);
Step 6 - HTML page with auto-refresh
Now the interface. This snippet consumes api.php and updates the indicators every 30 seconds, without reloading the page:
<!-- status/index.html -->
<div id="status-painel" class="status-painel">
<div class="status-header">
<span id="status-geral" class="badge">Checking...</span>
<span id="status-online">- players online</span>
</div>
<ul id="status-lista"></ul>
<small id="status-hora"></small>
</div>
<script>
async function atualizarStatus() {
try {
const r = await fetch('/status/api.php', { cache: 'no-store' });
const d = await r.json();
// General indicator: online if any game server responds
const algumOnline = Object.values(d.servicos).some(s => s.online);
const geral = document.getElementById('status-geral');
geral.textContent = algumOnline ? 'ONLINE' : 'OFFLINE';
geral.className = 'badge ' + (algumOnline ? 'on' : 'off');
document.getElementById('status-online').textContent =
d.online + ' players online';
// Service list
const lista = document.getElementById('status-lista');
lista.innerHTML = '';
for (const chave in d.servicos) {
const s = d.servicos[chave];
const li = document.createElement('li');
const ping = s.online ? ` (${s.latencia} ms)` : '';
li.innerHTML = `<span class="dot ${s.online ? 'on' : 'off'}"></span>
${s.nome}: <strong>${s.online ? 'Online' : 'Offline'}</strong>${ping}`;
lista.appendChild(li);
}
document.getElementById('status-hora').textContent =
'Updated: ' + new Date(d.atualizado).toLocaleTimeString('en-US');
} catch (e) {
document.getElementById('status-geral').textContent = 'ERROR';
}
}
atualizarStatus();
setInterval(atualizarStatus, 30000); // every 30s
</script>
Step 7 - Styling the indicators
A bit of CSS makes the panel readable at a glance. The green/red dot is what the eye looks for:
.status-painel { max-width: 420px; font-family: system-ui, sans-serif; }
.badge { padding: 3px 10px; border-radius: 4px; font-weight: 700; }
.badge.on { background: #113d1a; color: #35d05a; }
.badge.off { background: #3d1111; color: #ff5c5c; }
.status-lista, #status-lista { list-style: none; padding: 0; }
#status-lista li { padding: 6px 0; display: flex; align-items: center; gap: 8px; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.dot.on { background: #35d05a; box-shadow: 0 0 6px #35d05a; }
.dot.off { background: #ff5c5c; }
Step 8 - Uptime history (optional, but recommended)
A snapshot shows the "now". To display "99.3% uptime over the last 7 days", record the result periodically in a table. Set up a cron every minute calling a writer script:
<?php
// status/gravar-uptime.php (run via cron every 1 minute)
require_once __DIR__ . '/snapshot.php';
require_once __DIR__ . '/db.php';
$s = gerarSnapshot();
$algumOnline = false;
foreach ($s['servicos'] as $svc) {
if ($svc['online']) { $algumOnline = true; break; }
}
$conn = getMuDB();
sqlsrv_query($conn,
"INSERT INTO StatusHistorico (checado_em, online, jogadores) VALUES (GETDATE(), ?, ?)",
[[$algumOnline ? 1 : 0, SQLSRV_PARAM_IN], [$s['online'], SQLSRV_PARAM_IN]]
);
-- Table for the history (SQL Server)
CREATE TABLE StatusHistorico (
id INT IDENTITY(1,1) PRIMARY KEY,
checado_em DATETIME NOT NULL,
online BIT NOT NULL,
jogadores INT NOT NULL DEFAULT 0
);
GO
-- Uptime over the last 7 days (percentage)
SELECT
CAST(SUM(CASE WHEN online = 1 THEN 1 ELSE 0 END) AS FLOAT)
/ COUNT(*) * 100 AS uptime_pct
FROM StatusHistorico
WHERE checado_em >= DATEADD(DAY, -7, GETDATE());
On Linux cron, the line would be * * * * * php /var/www/site/status/gravar-uptime.php. On Windows, use Task Scheduler pointing to php.exe with the script's path.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Page freezes for several seconds | fsockopen with no timeout or a high timeout | Use a 2-3s timeout; enable the snapshot cache |
| Always shows offline even with the server up | Firewall blocking the port from the site host | Open the port to the site's source IP |
| Shows online but 0 players | Connection-status field different from the one used | Adjust the table/field name to your schema |
| Count "stuck" after a crash | GameServer did not reset ConnectStat at startup | Reset the field at boot or use your own status table |
sqlsrv_connect fails intermittently | Page checking the database on every request | Enable caching; handle the exception and show "DB offline" |
| Latency always high | DNS resolution on the host | Use a direct IP instead of a hostname in the checker |
Security and best practices
- Never expose credentials in the public JSON.
api.phpshould only return status and count, never connection strings or internal details. - Restrict the ports by IP. The server's administrative port should not be open to the world just for the page to work - open it only to the site's IP.
- Handle the database being offline gracefully. If
getMuDB()throws an exception, show "Database under maintenance" instead of a fatal PHP error that leaks server paths. - Caching is mandatory in production. Without it, a spike in visits becomes a spike in TCP connections and queries on your GameServer.
- Avoid false positives. An open port does not guarantee login works. If you can, combine the port test with the database check for a more honest "healthy" status.
Launch checklist
- ConnectServer and GameServer ports confirmed in the
.inifiles - Firewall opening the ports from the site host to the game host
- Port test function with a 2-3 second timeout
- Player count validated against the real database schema
- Snapshot cache active (TTL 15-30s)
api.phpendpoint returning JSON with no sensitive data- Auto-refresh on the front-end (30-60s)
- Graceful handling for the database being offline
- Cron recording uptime history every minute
- Real test: take the GameServer down and confirm it turns "Offline"
- Real test: bring it back up and confirm it returns to "Online"
With these steps, your status page becomes a trustworthy storefront for the server: fast, resilient to outages and honest about the real state of each service. It is the kind of detail that separates an amateur project from a server that players take seriously.
Frequently asked questions
Does the status page need to run on the same server as the game?
Not necessarily. It can run on any host that can reach the ConnectServer and GameServer ports over TCP, or read the database. Many admins host the site on a separate VPS and monitor the game server via its public IP - you just need to open the ports on the firewall.
How do I know which port to check for the online status?
The default ConnectServer port is usually 44405 and the GameServer's is 55901, but this varies by version and by how you configured ConnectServer.ini and GameServer.ini. Always confirm in your configuration files before coding the checker.
Does the port test freeze the page when the server is offline?
It does if you don't set a timeout. Always use a short timeout (2 to 3 seconds) in fsockopen or stream_socket_client. Without a timeout, a port filtered by the firewall can leave the request hanging for 30 seconds or more.
Can I count online players without accessing the server's memory?
Yes. The most stable way is to count records in the character or account table with the connection-status field marked as online (ConnectStat=1 or similar). The exact field name varies by MuServer/IGCN/Season version.
How often should I refresh the status?
For the visitor, an auto-refresh every 30 to 60 seconds is enough. On the backend, use a cache of 15 to 30 seconds so you don't check the ports on every request and don't overload the GameServer with queries.