TPTito Pinardo Gutiérrez
← Back to the rack
Español
1U · BATTLEPROSRust · Folia · PostgreSQL Open for external testing

Slot 12 of 26 in the rack · In production

1U unit · Ongoing personal project

BattlePros

Open for testing · 0.7.45

A custom-built Minecraft network: one world split across several servers, no loading screens when you cross between them, and everything behind it (gateway, controller, agents, panel) written from scratch.

My part
The idea, the architecture, the decisions and the testing. I write the code with an AI agent as a teammate, under fixed rules: measure before and after, document why, and never change the gameplay.
Where it lives
On this same server: the controller and shards in one container, the public gateway isolated in the DMZ in another. Nothing on my PC.
Testing vs production
Everything was tested with two zones (two “continents” 80 ms apart) to prove the world split, crossings and high availability work. In production, for external testing, there is just one: this server's goliath.
  • 20 TPSwith 100 bots at once in the zone
  • −80 %on paying another player (/pay), after merging 5 database round trips into 1
  • 2 → 1zones: two in testing, one in production
  • 0holes in the account impersonation test suite
  • 1 commandto add a new physical server to the network
  • 2.35×faster terrain generation with the Rust generator

Inside the network

Click a part or pick a flow: joining, crossing to another server with no loading screen, adding a server with one command…

Outside One goliath · Proxmox containers one zone in production DMZ · the only part visible from the internet Control · behind single sign-on Shards · the world, split World, agents and backups Player Mojang DNS (SRV) Router Another player Me (admin) Forgejo CI Another server Gateway (Rust) Voice Firewall Authelia Panel Controller PostgreSQL eu-1 eu-2 Colosseum Generator World store Agent Backups Metrics
Hermesvisiting the unit

Click a part or pick a flow. If you came from a skill, the parts where I use it are already lit.

      You join the server
      1. Player — You type the domain into Minecraft.
      2. DNS with an SRV record — DNS answers with an SRV record: which port to use.
      3. Router — The router only lets that port through, and only to the DMZ.
      4. DMZ firewall — The DMZ firewall lets the gateway talk to just what it needs.
      5. The gateway (Rust) — The gateway encrypts the connection and asks your client to identify itself.
      6. Mojang sessions — Mojang confirms the account is yours and sends your skin.
      7. The gateway (Rust) — If the network is full (the slots set in the panel), you're told here.
      8. The controller (Rust) — The controller knows which shard owns the area where you were.
      9. The gateway (Rust) — The gateway connects you to it.
      10. Shard eu-1 (Folia) — eu-1 loads your data from the database.
      11. Player — You're playing.
      You cross to another server with no loading screen
      1. Player — You walk towards an area owned by another shard.
      2. Shard eu-1 (Folia) — eu-1 sees you approaching the edge of its cell.
      3. The controller (Rust) — The controller confirms who owns the next cell.
      4. The gateway (Rust) — Underneath, the gateway opens a connection to eu-2…
      5. Shard eu-2 (Folia) — …which already has you ready on the other side.
      6. World store (Rust) — eu-2 loads the terrain from the shared world store.
      7. Shard eu-2 (Folia) — The gateway switches the packet stream on the fly: a splice.
      8. Player — You keep walking. You never saw a loading screen.
      You talk to someone on another server
      1. Player — You type in chat.
      2. The gateway (Rust) — The gateway passes it to your shard.
      3. Shard eu-1 (Folia) — eu-1 filters it (swearing, spam, muted players) and shows it to players here.
      4. PostgreSQL — PostgreSQL fans it out with LISTEN/NOTIFY to the other shards.
      5. Shard eu-2 (Folia) — eu-2 renders it as if it were its own.
      6. The gateway (Rust) — It arrives as a system message: no “cannot be verified” warning.
      7. Another player — Your friend reads it, even on another server.
      I add a physical server with one command
      1. Administrator — I open the panel from home.
      2. Authelia (sign-in) — Authelia checks it's me.
      3. Admin panel — In “Servers” I name the new server.
      4. The controller (Rust) — The controller creates a single-use, expiring token.
      5. Admin panel — The panel shows me the command, only once.
      6. Administrator — I copy it…
      7. A new physical server — …and paste it as root on the new server.
      8. The controller (Rust) — Before downloading anything, it checks the controller's CA fingerprint: nobody in the middle can swap the script.
      9. A new physical server — It installs Java, the user, the agent (checking its checksum) and the service.
      10. Agent (one per machine) — The agent signs up with the token and receives its certificate.
      11. The controller (Rust) — It shows up in the panel: I give it services and shards.
      I change the slots for the whole network
      1. Administrator — I want 300 players instead of 500.
      2. Authelia (sign-in) — Through Authelia.
      3. Admin panel — In “Network” I change the slots and save.
      4. The controller (Rust) — The controller logs it (who and when)…
      5. PostgreSQL — …and stores it in the database.
      6. The controller (Rust) — On the next heartbeat it sends it to every agent.
      7. Agent (one per machine) — The agent hands it to the gateway on its machine.
      8. The gateway (Rust) — The server list shows the new maximum and, when full, the gateway lets nobody new in.
      9. Another player — Those already inside keep playing: nobody gets kicked.
      A new version ships
      1. Forgejo (CI and releases) — A tag in Forgejo builds, tests and publishes the release with its checksum.
      2. The controller (Rust) — I pin the version in the panel: the controller downloads it and checks the checksum.
      3. Agent (one per machine) — The agents see their version is no longer the desired one.
      4. Shard eu-1 (Folia) — eu-1 warns in game, stops, updates and comes back…
      5. Agent (one per machine) — …and only when it's healthy does the next one go.
      6. Shard eu-2 (Folia) — eu-2 the same: one by one, never all at once.
      7. Agent (one per machine) — Meanwhile its players have moved to the shard that's still running.
      8. Colosseum (league) — The colosseum goes last.
      9. The controller (Rust) — The controller marks it done when all are running the new version.
      A shard goes down
      1. Shard eu-2 (Folia) — eu-2 hangs or runs out of memory.
      2. Agent (one per machine) — Its agent stops seeing it healthy and reports it.
      3. The controller (Rust) — The controller sees cells with nobody running them.
      4. PostgreSQL — It reassigns them in the database, in one go and with no duplicates.
      5. The controller (Rust) — It tells the remaining shards.
      6. Shard eu-1 (Folia) — eu-1 takes over eu-2's cells too.
      7. The gateway (Rust) — The gateway moves players to the shard that's still alive.
      8. Player — You lose a few seconds, not your game. The agent restarts eu-2.
      1. Player

        Anyone with an original Minecraft account. Mojang accounts only: nobody can join pretending to be someone else.

      2. Mojang sessions

        The gateway asks Mojang whether the account is real (and gets the skin). If Mojang doesn't answer in time, you don't get in.

      3. DNS with an SRV record

        Players type just the domain; an SRV record tells them which port to use. No Cloudflare proxy: Minecraft isn't HTTP.

        • Technitium DNS
      4. Router

        Forwards the game and voice ports only to the DMZ. From the DMZ, only the exact game ports are reachable.

        • MikroTik
        • VLANs
      5. Another player

        Someone elsewhere in the world, maybe on another server. To them it is the same world and the same chat.

      6. Administrator

        Me in the browser: the panel sits behind Authelia and is only reachable from home.

      7. Forgejo (CI and releases)

        Each part has its own repository: every push is built and tested, and a tag publishes a release with its SHA-256 checksum.

        • Forgejo · Actions
        • Git
      8. A new physical server

        Any Linux with systemd: paste one command and it installs itself (Java, user, agent and sign-up), checking the CA fingerprint first.

        • Linux (Debian, Alpine, Arch)
      9. The gateway (Rust)

        My own Minecraft proxy: encryption and Mojang login, player slots, and the splice that moves a player between servers with no loading screen.

        • Rust
        • Linux / SSH hardening
      10. Voice chat

        The UDP relay for proximity voice chat, in the same gateway: voice never touches the shards.

        • Rust
      11. DMZ firewall

        The gateway lives isolated in its own VLAN. If someone broke it, they could only reach the game ports, never the database or the rest of the house.

        • VLANs
        • Linux / SSH hardening
      12. Authelia (sign-in)

        The panel has no users of its own: it goes through the home single sign-on and the controller records who did what.

        • Linux / SSH hardening
      13. Admin panel

        A Svelte web app: servers, shards, players, staff, slots, network-wide announcements, versions, live map and monitoring.

        • JavaScript / TypeScript
      14. The controller (Rust)

        The brain: decides which server runs each piece of the world, stores the desired state, enrols machines with single-use tokens and rolls out versions.

        • Rust
        • PostgreSQL
      15. PostgreSQL

        Economy, homes, teams, punishments and the world split. Anything that gives or takes something is always checked here, in a single statement.

        • PostgreSQL
      16. Shard eu-1 (Folia)

        A game server running my build of Folia: it owns some cells of the world. With players nearby, the whole area stays on one shard so bases and redstone are never split.

        • Java (Paper/Folia)
      17. Shard eu-2 (Folia)

        Another piece of the same world. If eu-1 goes down, its cells move here and players keep playing.

        • Java (Paper/Folia)
      18. Colosseum (league)

        A shard just for the league: duels with kits, elo, challenges and seasons, without loading the main world.

        • Java (Paper/Folia)
      19. Terrain generator (Rust)

        Generates new terrain outside Folia, with the same seed and 99.3 % identical blocks. Folia only adds the structures on top.

        • Rust
      20. World store (Rust)

        Stores chunks compressed with zstd and a dictionary (7.5 times smaller). Any shard can load any area.

        • Rust
      21. Agent (one per machine)

        Starts, stops and updates the shards on its machine as the controller says, and sends it metrics every few seconds.

        • Rust
        • Linux (Debian, Alpine, Arch)
      22. Backups (PBS)

        Daily encrypted backups of the container and the database, plus the weekly restore test for the whole homelab.

        • Proxmox Backup Server
      23. Metrics

        TPS, MSPT, memory, CPU, network and disk for every shard and machine, stored in the controller itself: 14 days of charts in the panel.

        • PostgreSQL

      What I use from my skill tree

      Pick a skill: the parts where I use it light up on the map and I tell you what for.

      Rust
      Gateway, controller, agent, world store and generator: everything that isn't the game itself.
      Java (Paper/Folia)
      The game plugin on Folia 26.3: economy, homes, teams, league, staff tools and network-wide chat.
      PostgreSQL
      All game and world-split state; single statements so nothing duplicates, and LISTEN/NOTIFY to signal between servers.
      JavaScript / TypeScript
      The admin panel in Svelte 5 and TypeScript.
      Linux (Debian, Alpine, Arch)
      Debian containers with user-level systemd; the agent installs on any Linux with one command.
      Proxmox VE
      Both containers live on the home Proxmox, next to the rest of the homelab.
      LXC
      Controller and shards in one container; the gateway in another, isolated in the DMZ.
      Terraform
      The containers, their network and their DNS, declared in Terraform.
      Ansible
      Roles for the development container and the DMZ gateway, deployed from Forgejo.
      Forgejo · Actions
      One repository per part with CI and checksummed releases.
      Git
      A branch per change, a tag per version and a versioned results wiki.
      VLANs
      The public gateway in the DMZ VLAN, with no access to the rest of the house.
      MikroTik
      Forwarding for the game and voice ports, and DMZ rules to just the right ports.
      Technitium DNS
      An SRV record so you join with just the domain, created via API.
      Linux / SSH hardening
      Mojang accounts only, mTLS between services, database not exposed, and an impersonation suite with zero holes.
      Proxmox Backup Server
      Daily encrypted backups with a weekly restore test.

      Before and after, measured

      Every change is measured against the previous version with the same test (bots doing everything for 6 minutes) and only stays if it improves things without breaking anything. Some numbers come from the far test zone, 80 ms from the database: that's where round trips hurt most.

      before after

      Database

      • /pay (pay someone) −80 %

        707 ms 140 ms

        Five database round trips (look up the name, transaction, notices) merged into one statement.

      • /sethome (save a home) −69 %

        461 ms 144 ms

        Lock, count, insert, read and COMMIT: five round trips, now one.

      • /profile −65 %

        393 ms 138 ms

        Profile and balances in one query with subqueries instead of four.

      • Selling with 10 at once −69 %

        2768 ms 852 ms

        The whole sale and its COMMIT in one send: the price lock lasts one round trip.

      • /msg on the same server −96 %

        84 ms 3 ms

        If the other player is on the same shard, the message skips the database.

      Game server

      • TPS with 100 bots +9 %

        17.4 TPS 19 TPS

        Folia used a single region thread on 12 cores: now they're spread out.

      • MSPT with 100 bots −21 %

        33.7 ms 26.7 ms

        Same change: fewer milliseconds per tick and no global-region stall.

      • TPS at night with 40 players +53 %

        10.3 TPS 15.8 TPS

        AI activation ranges, spawning every N ticks and cheaper collisions.

      • Opening a database-backed menu −93 %

        270 ms 19 ms

        Display-only data in memory; anything that gives or takes, in the database.

      World, network and disk

      • Generating new terrain ×2.3

        23 chunks/s 54 chunks/s

        Terrain comes from a Rust service; Folia only adds the structures.

      • World compression +9 %

        6.9 × 7.5 ×

        zstd with a dictionary trained on real chunks (the End went from 17× to 25×).

      • Gateway CPU with 100 bots −34 %

        28.6 % 18.8 %

        A cache of already-compressed chunks: 93 % hits with 42 MB.

      • /pay p95 after a disk move −55 %

        465 ms 208 ms

        The test database moved from the system RAID 1 to the SAS RAID 10.

      Data from the project's results wiki (p50 unless stated). Tests ran with two zones to prove everything works across continents; production has just one.

      Incident log

      Real failures. Read the symptom, pick the cause you think it was and see how it was fixed.

      1. #1

        “Failed to connect: disconnected”

        29-10-2026 · Blocking: the server showed up in the list, but nobody could join

        From outside the server appeared in the list with its player count, but joining said “Failed to connect to the server: disconnected”. On the home network, with bots, everything worked.

        What do you think was going on?

        Show the fix
        Cause
        The new DMZ container inherited the home DNS as its first resolver. The DMZ can't reach it (by design), so every lookup waited 5 s before trying the next one. The server list doesn't ask Mojang, but joining does: it was too late and got cut off.
        Fix
        Public DNS for the DMZ container, in Terraform (not by hand), plus a note in the docs. The impersonation tests were rerun: before, they only “passed” because Mojang never answered.
        Lesson
        A test that passes for the wrong reason is worse than one that fails. Look at why it passes.
      2. #2

        “Message cannot be verified”

        29-10-2026 · Medium: a warning in every player's chat

        Minecraft warned that chat messages couldn't be verified, and some clients flagged them as unsafe.

        What do you think was going on?

        Show the fix
        Cause
        Behind a proxy, game servers can't check chat signatures: the gateway is what verifies the account. The client saw a server without secure chat and warned about it.
        Fix
        The gateway tells the client, in the login packet, that the server is online and enforces secure chat, and the shards send chat as system messages (unsigned), like chat coming from other shards.
        Lesson
        The client only knows what the server tells it: with a proxy in the middle, it has to tell it right.
      3. #3

        Selling a diamond took 3 seconds

        27-10-2026 · High: with 10 players selling at once

        With ten players selling at once, the median sale took 2.8 s and some nearly 5. The database was barely working.

        What do you think was going on?

        Show the fix
        Cause
        A sale opened a transaction, locked the material's price (so nobody over- or under-sells) and made several 80 ms round trips before COMMIT. Ten sales at once = ten in a queue.
        Fix
        The whole sale in a single SQL statement with the COMMIT in the same send: the lock lasts one round trip instead of five. From 2,768 ms to 852 median, and nobody over- or under-sells.
        Lesson
        With a distant database, queries aren't the cost: round trips are, especially with a lock held.
      4. #4

        The shard that took 40 minutes to die

        22-10-2026 · High: a shard hung mid-test

        With 100 bots exploring new terrain, a shard ran out of memory and took 40 minutes to shut down.

        What do you think was going on?

        Show the fix
        Cause
        Generating new chunks is the most expensive thing in Minecraft. With no per-player limit, a hundred explorers filled the generation queue faster than it drained.
        Fix
        A per-player generation limit (−20 % CPU, no bots kicked) and then my own Rust generator with the same seed: 2.35 times faster than Folia alone on the same machine.
        Lesson
        Before adding memory, find out what's piling up and why.
      5. #5

        Slow menus with a 1 ms query

        23-10-2026 · Medium: every menu took 115-270 ms to open

        Opening menus that read from the database (homes, profile…) took up to 270 ms, even though the query took under 1.5 ms.

        What do you think was going on?

        Show the fix
        Cause
        In Folia each region has its own thread. The query ran on another thread and the answer had to wait its turn on the player's region thread.
        Fix
        What is only displayed (homes, display caches) lives in memory; anything that gives or takes is still checked in the database. From 115-270 ms to 17-19.
        Lesson
        Measure where the time really goes: the query was the one thing that didn't need optimising.

      How it was built

      1. 20-22 Oct 2026

        One world, several servers

        • My own Rust gateway with the no-loading-screen splice.
        • Controller with the cell-based world split and agents that start and stop shards.
        • World store with zstd and a dictionary.
      2. 22-25 Oct 2026

        Performance, with data

        • Rust terrain generator with 99.3 % parity on the same seed.
        • Region threads, mobs and anti-xray measured one by one.
        • Results wiki: every change with its baseline and its decision.
      3. 25-28 Oct 2026

        Two continents

        • A second zone 80 ms away, with its own controller, a PostgreSQL replica and crossings between them.
        • Optimisation loop: merge database round trips and measure every command with 100 bots.
        • High availability: if the controller dies, another takes over and moves back next to its database.
      4. 29 Oct 2026

        Open to the public

        • Gateway in the DMZ, Mojang accounts only, and an impersonation audit.
        • A single zone in production and nothing on my PC.
        • New panel: servers with one command, network-wide slots and announcements.

      Decisions (and why)

      • Velocity or my own gateway

        My own gateway, in Rust

        Crossing between servers with no loading screen means splicing packets on the fly, and a generic proxy doesn't do that.

      • Paper, Purpur or Folia

        Folia, with my own patches

        Folia splits the world into regions with their own threads: with many players spread out it uses every core.

      • Redis in front of PostgreSQL

        No: fewer round trips

        33 hours of pg_stat_statements: the weight was writes that must persist. Merging round trips beat a cache.

      • Split the world by hand or automatically

        The controller does it

        By cells, never splitting an area with players in it (bases and redstone intact), and reassigning on its own if a server dies.

      • Expose the controller or just the gateway

        Just the gateway, in the DMZ

        The database isn't reachable from the internet and services talk to each other with mutual TLS.

      What I learned

      • Measure before touching: half the ideas that looked good improved nothing and were dropped with data.
      • With a distant database, the enemy is round trips, not queries.
      • A test that passes for the wrong reason is worse than one that fails.
      • Testing with two continents from day one forced everything to work without local shortcuts.

      What's next

      • More real players in external testing.
      • Move mob AI to a separate service without lowering TPS.
      • A second zone in production when there's hardware for it.
      • Version 1.0.0, signed off by the people who play it.

      Shall we talk?

      I can walk through any of these decisions in detail in an interview.

      Download CV Get in touch ← Back to the rack

      Rebuilt from the project's documentation and results wiki: no addresses, ports or internal names.