Game studios running session-based multiplayer titles have a recurring cost problem: dedicated servers sit idle between matches, cloud VMs take too long to boot for on-demand lobbies, and container orchestrators like Kubernetes carry a steep operational learning curve. Fly.io Machines, the hardware-virtualized VM platform behind Fly.io’s app runtime, has become a popular answer because it starts a fresh instance of your game server container in well under a second once the image is cached, bills per second instead of per hour, and can scale a fleet down to zero when nobody is playing. This tutorial walks through deploying a real, working multiplayer game server on Fly.io Machines, from your first flyctl command to a production-style autoscaling, multi-region setup.
The timing matters. OVHcloud told customers in August 2026 that renewal pricing on its latest-generation dedicated Game servers would rise by as much as 87%, with current-generation hardware up 51% and even older 2024-generation boxes climbing 28%, according to reporting from TechRadar. For studios that have been paying for fixed dedicated capacity around the clock, an elastic, pay-per-second alternative is suddenly a lot more attractive, and Fly.io Machines is one of the few platforms built specifically around ephemeral, fast-booting compute.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Is Fly.io Machines, and Why It Matters for Multiplayer Game Hosting in 2026
Fly.io Machines is the low-level compute primitive underneath every Fly.io app: a hardware-virtualized VM that boots from a Docker image, runs your process, and can be stopped, started, or destroyed programmatically through a REST API. Unlike a traditional EC2 instance or DigitalOcean droplet, a Machine is designed to be created and torn down constantly rather than left running. According to Fly.io’s own documentation, once a Machine’s image has already been fetched and its filesystem built, starting it again is “well under a second,” while the first-ever creation of a Machine (pulling the container and building the boot filesystem) takes closer to “low double-digit seconds.”
That speed profile lines up almost perfectly with how session-based multiplayer games behave. A player queues for a match, a fresh game server instance needs to exist within a second or two, the match runs for anywhere from five minutes to an hour, and then the server should disappear and stop costing money. Traditional autoscaling groups built for web traffic are not tuned for this churn pattern; Kubernetes-based tools like Agones solve it well but require you to run and operate a full cluster. Fly.io Machines gives you the same on-demand, ephemeral-instance model without a control plane to manage, because Fly operates the underlying Firecracker-based virtualization layer for you.
Fly.io currently runs Machines across 17 regions, including Ashburn, Chicago, Dallas, Los Angeles, San Jose, Toronto, Amsterdam, Frankfurt, London, Paris, Stockholm, Singapore, Tokyo, Sydney, Johannesburg, and São Paulo, per Fly’s published regions reference. For a multiplayer game, that geographic spread is the other half of the pitch: you can place a fresh Machine in whichever region is closest to a given match’s players, cutting round-trip latency without pre-provisioning capacity everywhere.
It also helps to be clear about what Fly.io Machines is not. It isn’t a game-specific product the way AWS GameLift or Unity’s Multiplay are; there’s no built-in matchmaking queue, no session-based fleet abstraction, and no SDK that understands the concept of a “match.” You get a fast, programmable VM and a REST API, and you build the game-specific layer (matchmaking, region selection, player-to-server routing) yourself. That’s a deliberate tradeoff: less built-in gaming tooling in exchange for a simpler, more general-purpose platform that doesn’t lock your architecture into one vendor’s idea of how multiplayer should work. Teams that have already built a custom allocator on top of raw VMs or Docker hosts will find the migration path to Machines fairly mechanical, since the core idea (create an instance per match, destroy it when done) doesn’t change, only the API calls and billing granularity do.
Fly.io Machines vs AWS GameLift vs Traditional Game VPS Hosting
Before committing to Fly.io Machines, it’s worth seeing how the model compares to the two other common approaches: AWS GameLift’s managed fleets, and a traditional rented or self-managed VPS running a game server binary around the clock. Each has a different cost shape and operational burden, and the right choice depends heavily on how predictable your concurrent-player load is.
| Factor | Fly.io Machines | AWS GameLift Servers | Traditional Dedicated VPS |
|---|---|---|---|
| Billing model | Per-second, pay only while a Machine runs | Per instance-hour across a managed fleet | Fixed monthly or hourly rate regardless of use |
| Cold start for a new instance | Well under 1 second once image is cached | Minutes to provision a new fleet instance | N/A, server runs continuously |
| Scale-to-zero support | Native, via auto_stop_machines | Not native; fleets keep minimum capacity | Not applicable |
| Regions available | 17 regions as of late 2026 | Dozens of AWS regions and Local Zones | Depends entirely on provider |
| Operational overhead | Low: flyctl + REST API, no cluster to run | Moderate: fleets, aliases, queues, FlexMatch | Low setup, high manual maintenance over time |
| Bandwidth cost | $0.02-$0.12/GB egress by region | Free egress on generation-6+ instances | Often bundled, but capped |
| Matchmaking | Bring your own (build with Machines API) | FlexMatch: $20 per million player packages + $1/hr | Bring your own |
FlexMatch pricing and GameLift’s broader fleet model are published on AWS’s GameLift Servers pricing page, which is worth reading side by side with Fly’s numbers if your studio is choosing between the two rather than committing upfront.
The practical takeaway: GameLift Servers remains the stronger choice for large studios that want a fully managed matchmaking stack and already live inside AWS, while a traditional VPS still makes sense for a single always-on world server with stable population. Fly.io Machines wins specifically for spiky, session-based concurrency, where you’d otherwise be paying for idle capacity most of the day. If you’ve already deployed on AWS GameLift or run fleets on Kubernetes with Agones, this tutorial is a useful point of comparison rather than a wholesale replacement.
Prerequisites: Tools, Accounts, and Versions You Need
This walkthrough assumes you’re comfortable with the command line and have a basic game server process you want to containerize (we’ll use a small Node.js WebSocket server as a stand-in, but the same steps work for any language that can run in a Docker container). Confirm the following before starting.
| Requirement | Minimum Version | Notes |
|---|---|---|
| flyctl CLI | v0.4.111 or later | Latest release as of September 30, 2026, per the flyctl GitHub releases page |
| Docker Engine | 24.x or later | Used to build the game server image locally before pushing |
| Node.js (example server) | 20 LTS | Only needed if you’re following the included Node.js example |
| Fly.io account | N/A | Free signup; a payment method is required to create Machines beyond the free allowance |
| Fly.io organization API token | N/A | Needed for the Machines REST API allocator in Step 6 |
You’ll also want a game client or test harness that can open a WebSocket connection, since the example server in this guide listens for WebSocket traffic rather than a custom UDP protocol. The same Machines deployment pattern applies if your real game server speaks UDP; you’d simply expose a UDP service in fly.toml instead of an HTTP/WebSocket listener.
Step 1: Create a Fly.io Account and Install flyctl
Start by creating a Fly.io account if you don’t already have one, then install the flyctl command-line tool. On macOS and Linux, the quickest path is the official install script; on Windows, use the PowerShell one-liner or winget.
# macOS / Linux
curl -L https://fly.io/install.sh | sh
# Windows (PowerShell)
iwr https://fly.io/install.ps1 -useb | iex
# Verify the install and version
flyctl version
Confirm you’re on v0.4.111 or newer, the version published on the flyctl GitHub releases page as of late September 2026. Fly ships flyctl updates frequently, so if your version is older, run flyctl version update before continuing. Minor API changes in the Machines endpoints have landed in recent releases, and an outdated client occasionally produces confusing config-validation errors.
Step 2: Authenticate and Scaffold Your Fly App
Log in, then create a new Fly app without deploying yet. Keeping the app scaffold separate from your first deploy gives you a chance to review the generated fly.toml before anything goes live.
flyctl auth login
flyctl launch --no-deploy --name my-game-server-fleet
cd my-game-server-fleet
ls
# Dockerfile fly.toml
If flyctl launch detects a Node.js project, it will generate a starter Dockerfile automatically. We’ll replace it in the next step with one tuned for a long-lived WebSocket process rather than a typical stateless web app.
Step 3: Containerize a Game Server With a Dockerfile
Here’s a minimal Node.js WebSocket game server and the Dockerfile that packages it. In a real project, swap server.js for your engine’s dedicated-server build (Unity headless Linux build, Godot server export, or a custom C++/Go binary).
// server.js
const { WebSocketServer } = require('ws');
const port = process.env.PORT || 8080;
const matchId = process.env.MATCH_ID || 'unassigned';
const wss = new WebSocketServer({ port });
const players = new Set();
wss.on('connection', (socket) => {
players.add(socket);
console.log(`Player joined match ${matchId}, total: ${players.size}`);
socket.on('message', (data) => {
for (const peer of players) {
if (peer !== socket && peer.readyState === peer.OPEN) {
peer.send(data);
}
}
});
socket.on('close', () => {
players.delete(socket);
if (players.size === 0) {
console.log(`Match ${matchId} empty, exiting for scale-to-zero`);
process.exit(0);
}
});
});
console.log(`Match ${matchId} listening on ${port}`);
# Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY server.js ./
EXPOSE 8080
CMD ["node", "server.js"]
Notice the server calls process.exit(0) once the last player disconnects. That’s deliberate: it lets Fly’s Machine lifecycle (configured in the next step) detect that the process has stopped and release the Machine, instead of leaving an empty match consuming per-second billing indefinitely.
Step 4: Configure fly.toml for Machines Deployment
The fly.toml file controls how Fly runs your Machines, including size, auto-stop behavior, and the minimum number kept warm. For a match-based game server, you typically want min_machines_running at zero so idle capacity costs nothing between matches.
# fly.toml
app = "my-game-server-fleet"
primary_region = "iad"
[build]
[env]
PORT = "8080"
[[services]]
internal_port = 8080
protocol = "tcp"
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[services.ports]]
port = 8080
handlers = ["tls", "tcp"]
[[vm]]
size = "shared-cpu-1x"
memory = "256mb"
Keep in mind Fly’s memory rules: shared-CPU Machines need memory in multiples of 256 MB, while performance-CPU Machines need multiples of 2 GB, with an overall ceiling of 2 GB per shared vCPU or 8 GB per performance vCPU. Extra memory itself got pricier in Fly’s pricing update effective October 1, 2026, which raised the per-GB add-on rate from $5 to $6 a month, so a shared-cpu-1x Machine bumped up to a full 1 GB of RAM now runs about $6.70/month under the current rates. For a small authoritative WebSocket relay like the example above, shared-cpu-1x with 256 MB is usually enough; a physics-heavy simulation will need to move up to performance-1x.
Step 5: Launch Your First Machine With flyctl
With the Dockerfile and fly.toml in place, deploy the app once to create the base image and confirm a single Machine boots correctly.
flyctl deploy
flyctl machine list
flyctl logs
You should see one Machine in a started state. Connect a WebSocket test client to the app’s assigned hostname on port 8080 to confirm the handshake succeeds before moving on to dynamic, per-match allocation.
Step 6: Build a Match Allocator With the Machines REST API
A single long-running Machine is fine for testing, but the entire point of this architecture is spinning up a dedicated Machine per match on demand. Fly exposes a Machines REST API for exactly this; your matchmaking service calls it to create a new Machine, passing match-specific environment variables like MATCH_ID.
// allocator.js — call this from your matchmaking backend
const FLY_API_TOKEN = process.env.FLY_API_TOKEN;
const APP_NAME = 'my-game-server-fleet';
async function createMatchMachine(matchId, region = 'iad') {
const res = await fetch(`https://api.machines.dev/v1/apps/${APP_NAME}/machines`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${FLY_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
region,
config: {
image: 'registry.fly.io/my-game-server-fleet:latest',
env: { MATCH_ID: matchId, PORT: '8080' },
guest: { cpu_kind: 'shared', cpus: 1, memory_mb: 256 },
services: [{
protocol: 'tcp',
internal_port: 8080,
ports: [{ port: 8080, handlers: ['tls', 'tcp'] }],
}],
},
}),
});
if (!res.ok) throw new Error(`Machine create failed: ${res.status}`);
return res.json();
}
Create a Fly API token scoped to this app (flyctl tokens create deploy) rather than reusing your personal account token, and store it as a secret in whatever service runs your matchmaker. One important platform limit to design around: Fly’s Machines API documentation specifies a rate limit of one request per second per action, meaning rapid-fire match creation needs to be queued rather than fired in a tight loop.
Step 7: Secure Machine-to-Machine Traffic With Private Networking
If your architecture splits matchmaking, a database, and game server Machines into separate Fly apps, they can reach each other over Fly’s private WireGuard-based network (6PN) without exposing internal ports to the public internet. Each Machine gets a private IPv6 address automatically, reachable at <app-name>.internal from any other Machine in the same organization.
# From inside a Machine, reach your matchmaking service privately
curl http://matchmaker.internal:3000/health
# Or resolve a specific app's Machines for internal service discovery
dig +short aaaa my-game-server-fleet.internal
This matters for game backends specifically because it keeps your matchmaking API, session database, and anti-cheat validation service off the public internet entirely, while still letting freshly created game-server Machines register themselves back to the matchmaker the moment they boot.
Step 8: Attach Persistent State With Fly Volumes
Most match servers are stateless and fine to destroy after the game ends, but if you need to persist replay data, player stats snapshots, or a world save between restarts, attach a Fly Volume. Volumes are billed at $0.15 per provisioned GB per month, and that charge applies to the provisioned size even if the Machine is stopped or the volume briefly unattached, so don’t over-provision “just in case.” If periodic backups are enough instead of a constantly attached volume, Fly’s current snapshot pricing (confirmed as of October 20, 2026) runs $0.08 per GB per month, with the first 10 GB of snapshots free every month, which comfortably covers rolling replay backups for a modest fleet.
flyctl volumes create match_data --region iad --size 1
# Reference it in fly.toml
# [[mounts]]
# source = "match_data"
# destination = "/data"
New organizations get 3 GB of total provisioned volume capacity free, which is enough for small persistent logs or save-state snapshots across a handful of Machines, but it will not cover a fleet-wide replay archive at scale.
Step 9: Place Machines Globally for Low-Latency Matches
Because the allocator from Step 6 accepts a region parameter, your matchmaking logic can pick the Fly region closest to the match’s players before creating the Machine. A simple approach is to measure latency from each connecting client to a handful of Fly regions and create the match Machine in whichever region scores lowest across the matched group.
const FLY_REGIONS = ['iad', 'lax', 'lhr', 'fra', 'sin', 'syd', 'gru'];
function pickRegion(playerPings) {
// playerPings: { iad: 42, lax: 88, ... } averaged across the match's players
return Object.entries(playerPings)
.filter(([region]) => FLY_REGIONS.includes(region))
.sort((a, b) => a[1] - b[1])[0][0];
}
This is the same problem Unity’s relay and lobby service solves with its own region list, and the logic transfers directly if you’ve worked with Unity Multiplay’s relay and lobby setup before; you’re just substituting Fly regions for Unity’s relay locations.
Step 10: Scale to Zero and Autoscale Based on Player Load
The auto_stop_machines = "stop" setting from Step 4 already lets individual match Machines shut themselves down once the process exits. For a persistent lobby or matchmaking-queue service that needs at least one instance always warm, set min_machines_running to 1 instead of 0, and let Fly handle the rest.
[[services]]
internal_port = 3000
protocol = "tcp"
auto_stop_machines = "suspend"
auto_start_machines = true
min_machines_running = 1
The difference between "stop" and "suspend" matters for game servers: stop fully shuts the Machine down (cheapest, but a cold restart), while suspend freezes memory state to resume faster, at the cost of some storage billing while suspended. For per-match servers that intentionally exit, stop is almost always the right call since there’s nothing worth preserving in memory once a match ends.
Step 11: Add Health Checks, Restarts, and Graceful Shutdown
Add an HTTP health check endpoint alongside your WebSocket listener so Fly can detect a hung Machine and restart it automatically, and handle SIGTERM so in-progress matches get a chance to notify connected players before the process dies during a deploy or manual stop.
process.on('SIGTERM', () => {
console.log('Received SIGTERM, notifying players and closing match');
for (const peer of players) {
peer.send(JSON.stringify({ type: 'server_shutdown', reason: 'maintenance' }));
}
wss.close(() => process.exit(0));
});
[[services.http_checks]]
interval = "15s"
timeout = "4s"
grace_period = "10s"
method = "GET"
path = "/health"
Without a graceful shutdown handler, a deploy or host maintenance event will hard-kill every active match simultaneously, which is one of the fastest ways to generate angry player reviews after a platform migration.
Step 12: Monitor Machines With Logs and Metrics
Before sending real player traffic to the fleet, wire up visibility into Machine lifecycle events, memory pressure, and crash loops. flyctl gives you a live log stream and basic metrics out of the box; for anything beyond a handful of concurrent matches, pipe logs to an external aggregator.
flyctl logs --app my-game-server-fleet
flyctl machine status <machine-id>
flyctl dashboard metrics
Watch specifically for Machines stuck in a starting state for more than a few seconds; that’s usually a sign your Docker image grew too large for a fast cold boot, or a health check is misconfigured and blocking the Machine from ever reporting healthy.
Step 13: Load Test and Validate Before Launch
Finally, simulate real match-creation churn before opening the fleet to players. Script the allocator to spin up and tear down dozens of match Machines back-to-back, respecting the one-request-per-second-per-action API limit, and confirm billing lines up with your expectations in the Fly.io dashboard before scaling further.
# Simple serial load test respecting the 1 req/sec Machines API limit
for i in $(seq 1 20); do
curl -s -X POST https://api.machines.dev/v1/apps/my-game-server-fleet/machines \
-H "Authorization: Bearer $FLY_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"region\":\"iad\",\"config\":{\"image\":\"registry.fly.io/my-game-server-fleet:latest\"}}"
sleep 1.1
done
Confirm old match Machines are actually exiting and being billed only for their active seconds; if you see Machines lingering in a stopped state for hours, clean them up with a scheduled flyctl machine destroy sweep, since stopped Machines still incur root-filesystem storage charges at $0.15 per GB per month.
Rolling Out Game Server Updates Without Dropping Live Matches
Because every match runs on its own Machine, shipping a game server patch doesn’t have to mean a maintenance window. Build and push a new image version, but don’t touch Machines that already have an active match running on the old version; instead, point the allocator in Step 6 at the new image tag so every match created from this moment forward uses the patched build, while existing matches finish out their session on the version they started with.
# Build and tag the new game server version
flyctl deploy --image-label v1.4.2 --no-public-url
# Update the allocator's image reference
# (allocator.js) image: 'registry.fly.io/my-game-server-fleet:v1.4.2'
# Old matches keep running on v1.4.1 until they naturally end;
# destroy any that are still open past a safe cutoff window
flyctl machine list --json | jq '.[] | select(.image_ref.tag=="v1.4.1")'
This per-match versioning approach sidesteps the classic multiplayer deployment problem of kicking every connected player mid-session just to roll out a balance patch. It does mean your fleet will briefly run two image versions side by side, so keep your network protocol backward-compatible for at least one release cycle, or tag messages with a protocol version the client can check before connecting.
How Reliable Is Fly.io for Production Game Hosting?
No platform is immune to incidents, and it’s worth building your allocator with that assumption rather than discovering it during a launch weekend. Fly’s public status history logged a Machines API incident on March 2, 2026 that affected customer deployments and the Fly dashboard before being resolved; it’s a useful reminder that the control plane (the API you call to create and destroy Machines) is a separate system from the Machines themselves, and a control-plane outage can block new match creation even while already-running matches continue unaffected.
For a production matchmaking service, that distinction should shape your error handling: if a Machine-create call fails, don’t assume the whole platform is down and don’t retry in a hot loop that just adds to the rate-limit pressure covered in Step 6. Instead, queue the request, back off exponentially, and surface a “finding a match, hang tight” state to the player rather than an immediate failure. Studios running Fly.io at scale typically pair this with a secondary region or secondary provider fallback for their matchmaking queue specifically, even if the game servers themselves stay single-provider, since the queue is the single point of failure that blocks every new match if it goes down.
Fly.io Machines Pricing for Game Server Hosting
Fly.io bills Machines per second while they’re running, which is the entire appeal for match-based workloads. Fly published an updated price list effective October 1, 2026, and the table below reflects those current rates for the sizes most relevant to game servers, drawn from Fly’s official pricing documentation, plus the storage and bandwidth costs that round out a realistic monthly estimate. Every app still gets a shared IPv4 address and unlimited Anycast IPv6 at no extra charge, though a dedicated IPv4 address now costs $2/month if a particular game server needs one.
| Resource | Rate | Approx. Monthly if Always-On |
|---|---|---|
| shared-cpu-1x, 256 MB | $0.0030/hour | ~$2.19 |
| performance-1x, 2 GB | $0.0458/hour | ~$33.00 |
| Additional memory | $6.00/GB/month | Scales with size |
| Fly Volumes (provisioned) | $0.15/GB/month | Billed even when unattached |
| Egress, North America/Europe | $0.02/GB | Varies with player traffic |
| Egress, Asia-Pacific/Oceania/South America | $0.04/GB | Varies with player traffic |
| Egress, Africa/India | $0.12/GB | Varies with player traffic |
The real cost advantage only shows up when you actually scale to zero between matches. A match server that runs for 20 minutes per session, 200 times a month, on a shared-cpu-1x Machine, costs a little over $0.01 per session in compute, roughly $2 a month total, versus $2.19 for the same Machine size left running continuously all month under Fly’s rates effective October 1, 2026. At low session volumes that gap looks small; it compounds fast once you’re running hundreds of concurrent matches, especially for any server tier that’s been bumped past its memory preset and is now paying the current $6/GB/month add-on rate, since idle capacity under a fixed-fleet model doesn’t disappear the way it does with per-second Machines. Fly also offers a 40% discount for customers who reserve a block of compute time in a specific region, worth evaluating once your match volume becomes predictable. New organizations additionally get up to three shared-cpu-1x Machines with 256 MB free, along with 160 GB of free outbound transfer, which is enough to fully prototype this setup at no cost.
Common Pitfalls When Deploying Game Servers on Fly.io Machines
- Forgetting to exit the process after the match ends. If your game loop never calls
process.exit()or equivalent,auto_stop_machineshas nothing to react to and the Machine runs (and bills) indefinitely. - Over-provisioning memory “to be safe.” Memory must be a multiple of 256 MB for shared-CPU Machines, and every extra increment is billed at $6/GB/month across your whole fleet, so oversized defaults get expensive at volume.
- Ignoring the Machines API rate limit. Firing match-creation requests faster than one per second per action will get throttled; a matchmaking service under load needs a request queue, not a tight loop.
- Leaving stopped Machines undestroyed. A stopped Machine still bills for its root filesystem storage at $0.15/GB/month; a fleet that creates thousands of ephemeral match Machines needs a cleanup job, not just a stop call.
- Skipping a health check endpoint. Without one, Fly cannot tell a hung game server process from a healthy but quiet one, so crashed matches can sit undetected until a player complains.
- Assuming UDP works exactly like TCP services. If your real game protocol is UDP rather than the WebSocket example here, you need a
[[services]]block configured for UDP specifically; copying a TCP/TLS service definition won’t route UDP traffic.
Troubleshooting Fly.io Machines Game Server Deployments
| Symptom | Likely Cause | Fix |
|---|---|---|
| Machine stuck in “starting” state | Oversized Docker image or slow entrypoint script | Slim the base image, move heavy init work to build time |
| New Machines never pass health checks | Health check path or port misconfigured in fly.toml | Confirm path and internal_port match your server’s actual listener |
| Match server exits instantly on boot | Missing or incorrect environment variables (e.g. MATCH_ID) | Verify the allocator’s env payload matches what server.js expects |
| 429 errors from the Machines API | Exceeding the 1 request/second/action rate limit | Queue match-creation calls and add backoff retry logic |
| Players in distant regions report high latency | Match Machine created in the wrong region | Confirm the allocator’s region-picking logic runs before Machine creation, not after |
| Unexpected bandwidth charges | High player population in Africa/India egress tier | Review the per-region egress table and consider regional server placement |
| Billing higher than expected for an idle fleet | Stopped Machines left undestroyed, still billing storage | Add a scheduled sweep that destroys Machines stopped longer than a set threshold |
| Deploy kills active matches abruptly | No SIGTERM handler in the game server process | Add graceful shutdown logic and a grace_period in the health check config |
| WireGuard private networking unreachable between apps | Apps are in different Fly organizations | Private networking (6PN) only connects apps within the same organization |
Advanced Tips for Production Multiplayer Fleets
Once the basic allocator and lifecycle are working, a few refinements separate a prototype from something that holds up under real traffic. First, build a reconciliation job that periodically lists all Machines via the API and cross-references them against your matchmaking database; orphaned Machines (created successfully but never registered, or registered but the Machine silently died) are the most common source of billing surprises. Second, if you expect bursty regional demand, pre-warm a small pool of “standby” Machines in your busiest regions using min_machines_running, accepting a small always-on cost in exchange for shaving even the sub-second cold start off peak-hour matches.
Third, treat the 40% reserved-capacity discount as a lever you pull only after you have real usage data; committing to a reserved block before you know your regional distribution risks locking in capacity you don’t use. Finally, if your game needs to coordinate state across multiple Machines in real time (a persistent world rather than isolated matches), Fly.io Machines is the wrong primitive on its own; look instead at an architecture built around stateful edge objects like Cloudflare Durable Objects, or layer a dedicated state-sync service like SpacetimeDB on top of your Machines fleet.
It’s also worth instrumenting cost per match from day one rather than waiting until a monthly invoice surprises someone. Tag every Machine with the match ID and region at creation time, then join that against your billing export so you can see, for example, whether a specific game mode with longer average sessions is quietly costing three times as much per player as your core mode. Teams that skip this step tend to discover cost problems only after they’ve scaled past the point where a quick fix is possible, usually right around a marketing push that drives a spike in concurrent matches.
Complete Working Project Recap
Putting the pieces from this tutorial together, a minimal production-ready project consists of five files: server.js (the game server process with graceful shutdown), Dockerfile (the container build), fly.toml (the Machines runtime configuration with auto-stop and health checks), allocator.js (the Machines REST API client your matchmaking service calls), and a small cleanup script that destroys stale stopped Machines on a schedule. Deploy the base image once with flyctl deploy, then let your matchmaking backend call the allocator for every new match, passing a unique MATCH_ID and a region chosen from player latency data.
From here, the natural next step for most teams is wiring this fleet into an existing backend, whether that’s a serverless leaderboard for post-match results or a broader cloud infrastructure stack that also handles analytics and player data. The allocator pattern shown here is intentionally framework-agnostic, so it slots into a Node.js, Python, or Go matchmaking service without modification beyond swapping the HTTP client.
Frequently Asked Questions
Is Fly.io Machines cheaper than AWS GameLift for game servers?
It depends on your traffic pattern. For spiky, session-based games where servers spend significant time idle between matches, Fly.io’s per-second billing and native scale-to-zero typically cost less than a GameLift fleet sized for peak concurrency. For large, stable populations with predictable load, GameLift’s managed fleet discounts and free egress on generation-6+ instances can close or reverse that gap.
Can I run a UDP-based game server on Fly.io Machines?
Yes. Configure a [[services]] block in fly.toml with protocol = "udp" and the appropriate internal port instead of the TCP/TLS example used in this tutorial. The Machine lifecycle, autoscaling, and billing model work identically regardless of transport protocol.
How fast does a new Machine actually boot for a new match?
Once your image has already been pulled and cached, Fly’s documentation describes starting a Machine as “well under a second.” The very first creation of a brand-new Machine, which includes fetching the container image and building its boot filesystem, takes longer, on the order of low double-digit seconds.
What happens if I exceed the Machines API rate limit during a traffic spike?
Requests beyond one per second per action (such as Create Machine or Start Machine) will be throttled. Your matchmaking service should queue allocation requests and implement retry-with-backoff rather than assuming every call will succeed instantly during a launch-day spike.
Do stopped Machines still cost money?
Yes. A stopped Machine no longer incurs compute charges, but it continues to be billed for its root filesystem storage at $0.15 per GB per month until it is destroyed. Fleets that create many ephemeral match Machines need a cleanup process that destroys old stopped Machines rather than leaving them accumulating.
How many regions can I deploy game server Machines to?
Fly.io currently lists 17 regions in its reference documentation, spanning North America, Europe, Asia-Pacific, South America, and Africa. You can create Machines in any of them independently per match, which is what enables the latency-based region-picking pattern covered in Step 9.
Does Fly.io have a free tier for testing this setup?
New organizations get an allowance that includes up to three shared-cpu-1x Machines with 256 MB of memory each, 3 GB of total provisioned volume storage, and 160 GB of outbound data transfer, which is enough to fully build and test the project in this tutorial before any billing applies.
Should I use “stop” or “suspend” for match Machines?
For Machines that run a single match and exit cleanly, use stop, since there’s no in-memory state worth preserving once the match ends and it avoids the extra storage billing that comes with a suspended Machine. Reserve suspend for longer-lived services, such as a lobby or matchmaking-queue process, where resuming from a frozen memory state faster than a cold boot is worth the small ongoing storage cost.





