Skip to content

Instantly share code, notes, and snippets.

@obezuk
Last active September 3, 2025 05:28
Show Gist options
  • Save obezuk/f80d425548f0a7a97434ac12457cce43 to your computer and use it in GitHub Desktop.
Save obezuk/f80d425548f0a7a97434ac12457cce43 to your computer and use it in GitHub Desktop.
Factorio Server Health Check Worker

Factorio Server Health Check Worker

A Cloudflare Worker that checks the health of a Factorio server listed in the public multiplayer lobby.

It wraps the Factorio Matchmaking API with automatic SRV record resolution.
Useful for monitoring and dashboards — returns a 200 OK if the server is online, and 404 Not Found if it isn’t.


How It Works

  1. SRV Lookup
    Uses DNS-over-HTTPS (https://1.1.1.1/dns-query) to resolve: _factorio._udp.example.com
  2. Resolve Target
    Resolves the SRV target hostname to an IPv4 (or IPv6 if needed).
  3. Query Factorio Matchmaking API
    Calls https://wiki.factorio.com/Matchmaking_API (https://multiplayer.factorio.com/get-game-details/$IP:$PORT).
  4. Return Health
  • 200 OK → server is online (response includes JSON with server details)
  • 404 Not Found → server is offline or not visible in the public lobby

Disclaimer

This Worker was quickly generated using ChatGPT. It was built as a quick way to perform out-of-band monitoring of a self-hosted Factorio server on a dynamic IP using the public Matchmaking API.

export default {
async fetch(request, env, ctx) {
try {
const srvName = "_factorio._udp.example.com"; // CHANGE ME: Requires a DNS SRV Record, See https://wiki.factorio.com/Multiplayer#DNS_SRV_Records.
const dohBase = "https://1.1.1.1/dns-query";
// --- helpers ---
const doh = async (name, type) => {
const url = `${dohBase}?name=${encodeURIComponent(name)}&type=${encodeURIComponent(type)}`;
const res = await fetch(url, {
headers: { "accept": "application/dns-json" },
// Keep DoH results hot for a short time on the edge
cf: { cacheTtl: 20, cacheEverything: true }
});
if (!res.ok) throw new Error(`DoH ${type} query failed (${res.status})`);
return res.json();
};
const pickSrv = (answers) => {
// SRV RDATA: "priority weight port target"
// Prefer lowest priority; if tie, highest weight.
const parsed = answers
.filter(a => a.type === 33 && typeof a.data === "string")
.map(a => {
const [priStr, wtStr, portStr, ...targetParts] = a.data.trim().split(/\s+/);
const target = targetParts.join(" ").replace(/\.+$/, ""); // strip trailing dot
return {
priority: parseInt(priStr, 10),
weight: parseInt(wtStr, 10),
port: parseInt(portStr, 10),
target
};
});
if (parsed.length === 0) throw new Error("No SRV answers found");
parsed.sort((a, b) => a.priority - b.priority || b.weight - a.weight);
return parsed[0];
};
const firstA = (answers) => {
const a = answers.find(a => a.type === 1 && a.data);
return a?.data ?? null;
};
const firstAAAA = (answers) => {
const aaaa = answers.find(a => a.type === 28 && aaaa?.data); // will fail; fix below
return aaaa?.data ?? null;
};
// --- 1) SRV lookup ---
const srvResp = await doh(srvName, "SRV");
if (!srvResp.Answer || !Array.isArray(srvResp.Answer)) {
throw new Error("SRV lookup returned no Answer");
}
const srv = pickSrv(srvResp.Answer);
// --- 2) Resolve target to IP (A first, then AAAA) ---
const aResp = await doh(srv.target, "A");
let ip = (aResp.Answer && Array.isArray(aResp.Answer)) ? firstA(aResp.Answer) : null;
if (!ip) {
const aaaaResp = await doh(srv.target, "AAAA");
const answers = (aaaaResp.Answer && Array.isArray(aaaaResp.Answer)) ? aaaaResp.Answer : [];
const aaaaRec = answers.find(x => x.type === 28 && x.data);
ip = aaaaRec?.data ?? null;
}
if (!ip) throw new Error(`Could not resolve IP for ${srv.target}`);
// Wrap IPv6 literal for host:port
const hostForUrl = ip.includes(":") ? `[${ip}]` : ip;
// --- 3) Query Factorio get-game-details ---
const detailsUrl = `https://multiplayer.factorio.com/get-game-details/${hostForUrl}:${srv.port}`;
const detailsRes = await fetch(detailsUrl, {
// Factorio endpoint is public; set a small cache
cf: { cacheTtl: 10, cacheEverything: true }
});
// Pass through status & body; add friendly CORS
const body = await detailsRes.text();
const headers = new Headers(detailsRes.headers);
headers.set("access-control-allow-origin", "*");
headers.set("access-control-allow-methods", "GET, OPTIONS");
headers.set("access-control-allow-headers", "Content-Type");
headers.set("cache-control", "public, max-age=10");
return new Response(body, { status: detailsRes.status, headers });
} catch (err) {
return new Response(
JSON.stringify({ error: String(err?.message ?? err) }, null, 2),
{
status: 502,
headers: {
"content-type": "application/json",
"access-control-allow-origin": "*"
}
}
);
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment