See who is playing on your game servers, and who has. Plany records every connection made to a server and every player session — who joined, when, and when they left — worked out from the server's own output, with no SDK to integrate. GetPresence answers "who is on this server right now" in one call; ListPlayerSessions and ListConnections give the history.
Records that a connection to a game server was opened, forwarded or closed.
Called by Plany's connection proxy; you do not call this yourself. Idempotent on connection_id, so a repeated report updates one connection rather than recording a second.
parentstringRequiredprojects/{p}/locations/{l}/gameServers/{s}.
connection_idstringRequiredThe proxy's own id for this connection, stable across the open report and the close report. A ULID, minted when the socket was accepted.
STATE_OPENSTATE_CLOSEDport_namestringRequiredWhich of the game's ports it arrived on.
protocolstring"tcp" or "udp".
client_addressstringSource address as the proxy saw it, host:port.
upstream_addressstringWhere it was forwarded to, once there was somewhere to forward it.
woke_serverboolWhether the game server was not running when this connection arrived. See Connection.woke_server — it is an observation, not a claim about which connection won the race to start it.
open_timeTimestampWhen the socket was accepted. Set on both reports so a close that arrives without its open — a proxy that restarted, a report that was dropped — is still a complete record rather than a connection that began at close time.
close_timeTimestampclose_reasonConnection.CloseReasonCLOSE_REASON_CLIENT_CLOSEDCLOSE_REASON_SERVER_CLOSEDCLOSE_REASON_WAKE_TIMEOUTCLOSE_REASON_UPSTREAM_UNREACHABLECLOSE_REASON_EXPIREDwait_durationDurationHow long the player waited to be spliced onto a server.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations//gameServers/", "connection_id": "my-connection", "state": "STATE_OPEN", "port_name": "" }' \ api.plany.cloud:443 plany.session.v1.PlayerSessionService/ReportConnection{ "name": "projects//locations//gameServers//connections/my-connection", "game_server": "projects//locations//gameServers/", "state": "STATE_OPEN", "port_name": "", "protocol": "", "client_address": "", "client_ip": "", "upstream_address": "", "woke_server": false, "open_time": "2026-10-04T12:00:00Z", "close_time": "2026-10-04T12:00:00Z", "close_reason": "CLOSE_REASON_CLIENT_CLOSED", "wait_duration": "300s", "player_session": "projects//locations//gameServers//playerSessions/my-player-session"}Lists the connections made to one game server.
parentstringRequiredprojects/{p}/locations/{l}/gameServers/{s}.
One server, and no wildcard. A region's connections are the highest-churn
rows this service holds, and projects/- over them is a query that answers
slowly and tells nobody what they asked.
page_sizeint32page_tokenstringtime_rangeIntervalRestrict by open time.
active_onlyboolOnly connections still open.
port_namestringOnly connections that arrived on this port.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations//gameServers/", "page_size": 50 }' \ api.plany.cloud:443 plany.session.v1.PlayerSessionService/ListConnections{ "connections": [ { "name": "projects//locations//gameServers//connections/my-connection", "game_server": "projects//locations//gameServers/", "state": "STATE_OPEN", "port_name": "", "protocol": "", "client_address": "", "client_ip": "", "upstream_address": "", "woke_server": false, "open_time": "2026-10-04T12:00:00Z", "close_time": "2026-10-04T12:00:00Z", "close_reason": "CLOSE_REASON_CLIENT_CLOSED", "wait_duration": "300s", "player_session": "projects//locations//gameServers//playerSessions/my-player-session" } ], "next_page_token": ""}Lists the player sessions on one game server, newest first.
parentstringRequiredprojects/{p}/locations/{l}/gameServers/{s}.
page_sizeint32page_tokenstringtime_rangeIntervalRestrict by start time. A session that began before the window and is still running is included, because "who was on my server yesterday evening" is not answered by excluding the person who never left.
active_onlyboolOnly sessions still active.
player_keystringOne player, by the key their sessions are recorded under. See PlayerSession.player_key.
include_provisionalboolInclude sessions built from templates that have not been promoted.
Off by default, and that default is the point: a caller who did not think about provenance gets only sessions the platform is prepared to stand behind. Somebody watching a new game being learned asks for them deliberately.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "projects//locations//gameServers/", "page_size": 50 }' \ api.plany.cloud:443 plany.session.v1.PlayerSessionService/ListPlayerSessions{ "player_sessions": [ { "name": "projects//locations//gameServers//playerSessions/my-player-session", "game_server": "projects//locations//gameServers/", "game": "", "player": { "display_name": "My server", "player_id": "my-player", "id_type": "", "address": "" }, "player_key": "", "state": "STATE_ACTIVE", "start_time": "2026-10-04T12:00:00Z", "end_time": "2026-10-04T12:00:00Z", "end_reason": "END_REASON_DISCONNECTED", "duration": "300s", "confidence": "CONFIDENCE_CONFIRMED", "connection": "projects//locations//gameServers//connections/my-connection", "origin_event": "" } ], "next_page_token": ""}Returns who is on one game server right now.
The read everything else actually wants. A singleton rather than a list, because a caller asking "should I keep this server running" wants one number and would otherwise page through sessions to compute it — three times, in three services, differently.
namestringRequiredprojects/{p}/locations/{l}/gameServers/{s}/presence.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "name": "projects//locations//gameServers//presence" }' \ api.plany.cloud:443 plany.session.v1.PlayerSessionService/GetPresence{ "name": "projects//locations//gameServers//presence", "game_server": "projects//locations//gameServers/", "player_count": 0, "connection_count": 0, "waiting_connection_count": 0, "players": [ { "display_name": "My server", "player_id": "my-player", "id_type": "", "address": "" } ], "last_activity_time": "2026-10-04T12:00:00Z", "has_provisional_sessions": false}Returns presence for every game server in a project.
parentstringRequiredprojects/{p}/locations/{l}, or projects/-/locations/{l} for every
project in this region — internal callers only.
page_sizeint32page_tokenstringoccupied_onlyboolOnly servers with somebody on them or somebody waiting.
Almost always what a caller wants: the fleet is mostly hibernated servers with nobody on them, which is the product working.
grpcurl -H "authorization: Bearer $PLANY_API_KEY" \ -d '{ "parent": "", "page_size": 50 }' \ api.plany.cloud:443 plany.session.v1.PlayerSessionService/ListPresence{ "presences": [ { "name": "projects//locations//gameServers//presence", "game_server": "projects//locations//gameServers/", "player_count": 0, "connection_count": 0, "waiting_connection_count": 0, "players": [ { "display_name": "My server", "player_id": "my-player", "id_type": "", "address": "" } ], "last_activity_time": "2026-10-04T12:00:00Z", "has_provisional_sessions": false } ], "next_page_token": ""}PlayerSession is one person's stay on one game server.
Worked out from the server's own output, which is what makes this work for a game Plany has never seen: the server already announces every join and part, and Plany learns to read them.
Check confidence. While Plany is still proving the patterns for a new game, its sessions are provisional.
namestringIdentifierprojects/{p}/locations/{l}/gameServers/{s}/playerSessions/{session}.
The id is derived from the game server, the player and the join time, so one stay always has the same name.
game_serverstringOutput onlygamestringOutput onlyThe game that was running, carried through so a consumer can reason about a session without resolving the server.
Who. Every field on Player is optional because games disagree on what a
player is; see plany.gamelog.v1.Player.
display_namestringDisplay name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.
player_idstringStable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.
id_typestringWhich namespace player_id belongs to, e.g. "steam", "minecraft", "eos".
Two games can both log a 17-digit number and mean different things.
addressstringSource address, when logged.
A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.
player_keystringOutput onlyWhat this session is keyed on: the player's stable id where the game logs one, and its display name where it does not.
Surfaced rather than left implicit, because it is what "the same player" means here — and on a game that logs only a name, that is a weaker claim than it looks. A rename mid-session becomes two sessions.
STATE_ACTIVESTATE_ENDEDWhen the server said they joined — the log line's own time, not when the platform got round to reading it. Extraction can lag by minutes after an outage, and a session timed by extraction is wrong by exactly that much.
Unset while active.
END_REASON_DISCONNECTEDEND_REASON_SUPERSEDEDEND_REASON_SERVER_STOPPEDEND_REASON_SERVER_RESTARTEDEND_REASON_EXPIREDend_time - start_time, for an active session measured to now.
Whether this session may be acted on.
Copied from the event that opened it. PROVISIONAL means the template that recognised the join has not been promoted, so the session is published for anyone who wants to watch early and must not reach a bill.
CONFIDENCE_CONFIRMEDCONFIDENCE_PROVISIONALconnectionstringOutput onlyThe connection this session is believed to have arrived on.
Matched on source address within a window, and therefore a belief rather than a fact — which is why it is a link between two stored records and not a merge. Empty whenever the game did not log an address, which is most games.
origin_eventstringOutput onlyThe event this session was opened from, so a surprising session can be traced back to the line that caused it.
{ "name": "projects//locations//gameServers//playerSessions/my-player-session", "game_server": "projects//locations//gameServers/", "game": "", "player": { "display_name": "My server", "player_id": "my-player", "id_type": "", "address": "" }, "player_key": "", "state": "STATE_ACTIVE", "start_time": "2026-10-04T12:00:00Z", "end_time": "2026-10-04T12:00:00Z", "end_reason": "END_REASON_DISCONNECTED", "duration": "300s", "confidence": "CONFIDENCE_CONFIRMED", "connection": "projects//locations//gameServers//connections/my-connection", "origin_event": ""}Connection is one network connection made to a game server's address.
Transport-level: it records the connection, not who made it. Plany never reads a game's protocol, so a connection has no player, account or game data of its own — that comes from the matching player session.
namestringIdentifierprojects/{p}/locations/{l}/gameServers/{s}/connections/{connection}.
game_serverstringOutput onlyThe game server this connection was made to.
STATE_OPENSTATE_CLOSEDport_namestringOutput onlyWhich of the game's ports the connection arrived on — "game", "query", "rcon". A server browser probing the query port every thirty seconds is not somebody playing, and without this the two are the same row.
protocolstringOutput only"tcp" or "udp".
client_addressstringOutput onlySource address as the proxy saw it, host:port.
A personal datum and treated as one: it is here because it is the only
thing a connection and a log line have in common, which makes it the only
way the two halves of this service correlate at all. See client_ip.
client_ipstringOutput onlyThe host part of client_address, stored separately because it is what
correlation matches on — a game announces a player's address without the
ephemeral port, so the full pair never joins to anything.
upstream_addressstringOutput onlyWhere the proxy forwarded it, host:port on the game server's own private
address. Empty while the server is still starting, which is the state a
connection to a hibernated server spends most of its life in.
woke_serverboolOutput onlyWhether the game server was not running when this connection arrived.
The demand signal, and named for what the proxy can actually observe. It is not "this connection is the one that started the server" — which single connection won that race is Playpute's to know, and asking the proxy would be asking it to guess. Twenty friends joining a hibernated server at once all carry this, which is the honest description of what happened.
What it separates is the thing that matters: demand for a stopped server, from traffic to a running one. Almost every connection is the latter.
Unset while the connection is open.
CLOSE_REASON_CLIENT_CLOSEDCLOSE_REASON_SERVER_CLOSEDCLOSE_REASON_WAKE_TIMEOUTCLOSE_REASON_UPSTREAM_UNREACHABLECLOSE_REASON_EXPIREDHow long the player waited before being spliced to a server: the gap between accepting the socket and having somewhere to send it.
Recorded because it is the number the product is judged on. Hibernation only works if a restore is faster than a boot, and this is the only place the platform observes what a player actually experienced rather than what a snapshot benchmark says.
player_sessionstringOutput onlyThe player session this connection is believed to belong to, when one was matched. Empty is the common case and not an error — see the package comment, and PlayerSession.connection.
connectionsrepeated ConnectionNewest first.
namestringIdentifierprojects/{p}/locations/{l}/gameServers/{s}/connections/{connection}.
game_serverstringOutput onlyThe game server this connection was made to.
STATE_OPENSTATE_CLOSEDport_namestringOutput onlyWhich of the game's ports the connection arrived on — "game", "query", "rcon". A server browser probing the query port every thirty seconds is not somebody playing, and without this the two are the same row.
protocolstringOutput only"tcp" or "udp".
client_addressstringOutput onlySource address as the proxy saw it, host:port.
A personal datum and treated as one: it is here because it is the only
thing a connection and a log line have in common, which makes it the only
way the two halves of this service correlate at all. See client_ip.
client_ipstringOutput onlyThe host part of client_address, stored separately because it is what
correlation matches on — a game announces a player's address without the
ephemeral port, so the full pair never joins to anything.
upstream_addressstringOutput onlyWhere the proxy forwarded it, host:port on the game server's own private
address. Empty while the server is still starting, which is the state a
connection to a hibernated server spends most of its life in.
woke_serverboolOutput onlyWhether the game server was not running when this connection arrived.
The demand signal, and named for what the proxy can actually observe. It is not "this connection is the one that started the server" — which single connection won that race is Playpute's to know, and asking the proxy would be asking it to guess. Twenty friends joining a hibernated server at once all carry this, which is the honest description of what happened.
What it separates is the thing that matters: demand for a stopped server, from traffic to a running one. Almost every connection is the latter.
Unset while the connection is open.
CLOSE_REASON_CLIENT_CLOSEDCLOSE_REASON_SERVER_CLOSEDCLOSE_REASON_WAKE_TIMEOUTCLOSE_REASON_UPSTREAM_UNREACHABLECLOSE_REASON_EXPIREDHow long the player waited before being spliced to a server: the gap between accepting the socket and having somewhere to send it.
Recorded because it is the number the product is judged on. Hibernation only works if a restore is faster than a boot, and this is the only place the platform observes what a player actually experienced rather than what a snapshot benchmark says.
player_sessionstringOutput onlyThe player session this connection is believed to belong to, when one was matched. Empty is the common case and not an error — see the package comment, and PlayerSession.connection.
next_page_tokenstringplayer_sessionsrepeated PlayerSessionNewest first.
namestringIdentifierprojects/{p}/locations/{l}/gameServers/{s}/playerSessions/{session}.
The id is derived from the game server, the player and the join time, so one stay always has the same name.
game_serverstringOutput onlygamestringOutput onlyThe game that was running, carried through so a consumer can reason about a session without resolving the server.
Who. Every field on Player is optional because games disagree on what a
player is; see plany.gamelog.v1.Player.
display_namestringDisplay name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.
player_idstringStable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.
id_typestringWhich namespace player_id belongs to, e.g. "steam", "minecraft", "eos".
Two games can both log a 17-digit number and mean different things.
addressstringSource address, when logged.
A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.
player_keystringOutput onlyWhat this session is keyed on: the player's stable id where the game logs one, and its display name where it does not.
Surfaced rather than left implicit, because it is what "the same player" means here — and on a game that logs only a name, that is a weaker claim than it looks. A rename mid-session becomes two sessions.
STATE_ACTIVESTATE_ENDEDWhen the server said they joined — the log line's own time, not when the platform got round to reading it. Extraction can lag by minutes after an outage, and a session timed by extraction is wrong by exactly that much.
Unset while active.
END_REASON_DISCONNECTEDEND_REASON_SUPERSEDEDEND_REASON_SERVER_STOPPEDEND_REASON_SERVER_RESTARTEDEND_REASON_EXPIREDend_time - start_time, for an active session measured to now.
Whether this session may be acted on.
Copied from the event that opened it. PROVISIONAL means the template that recognised the join has not been promoted, so the session is published for anyone who wants to watch early and must not reach a bill.
CONFIDENCE_CONFIRMEDCONFIDENCE_PROVISIONALconnectionstringOutput onlyThe connection this session is believed to have arrived on.
Matched on source address within a window, and therefore a belief rather than a fact — which is why it is a link between two stored records and not a merge. Empty whenever the game did not log an address, which is most games.
origin_eventstringOutput onlyThe event this session was opened from, so a surprising session can be traced back to the line that caused it.
next_page_tokenstringpresencesrepeated PresenceCounts only. Presence.players is left empty here on purpose — a region's
worth of player names is a response measured in megabytes answering a
question about occupancy.
namestringIdentifierprojects/{p}/locations/{l}/gameServers/{s}/presence. A singleton.
game_serverstringOutput onlyplayer_countint32Output onlyActive sessions: players the server said were on it and did not say left.
connection_countint32Output onlyOpen connections, which is a different and usually larger number. A query socket is a connection and not a player, and a player whose game opens two sockets is one player.
waiting_connection_countint32Output onlyConnections open against a server with no active sessions at all.
The signal that matters while a server is starting: somebody is waiting and nobody is playing yet. Zero players and a non-zero count here is exactly "a hibernated server has demand", which is the state Playpute will act on.
Who, capped — see ListPresenceResponse. Empty on a fleet-wide listing, where the counts are the point and the names would be the response.
display_namestringDisplay name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.
player_idstringStable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.
id_typestringWhich namespace player_id belongs to, e.g. "steam", "minecraft", "eos".
Two games can both log a 17-digit number and mean different things.
addressstringSource address, when logged.
A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.
The most recent thing this service observed about the server: a join, a part, a connection either way.
The field an idle-shutdown policy should read, and deliberately not "last_session_end": a server with a player who joined an hour ago and has said nothing since is busy, and one whose last event was a disconnect two hours ago is not.
has_provisional_sessionsboolOutput onlyWhether any active session came from a template still being shadowed. A
caller about to act on player_count — bill it, or keep a server alive on
the strength of it — should refuse when this is true.
next_page_tokenstringPresence is who is on a game server right now.
A derived view rather than a stored one: it is the answer to the question every other service actually asks, which is never "list me the sessions" but "is anybody playing". Idle shutdown, the console's server list and the meter that will bill for play rather than uptime all want this one number, and each computing it from a session list is three places to get it wrong.
namestringIdentifierprojects/{p}/locations/{l}/gameServers/{s}/presence. A singleton.
game_serverstringOutput onlyplayer_countint32Output onlyActive sessions: players the server said were on it and did not say left.
connection_countint32Output onlyOpen connections, which is a different and usually larger number. A query socket is a connection and not a player, and a player whose game opens two sockets is one player.
waiting_connection_countint32Output onlyConnections open against a server with no active sessions at all.
The signal that matters while a server is starting: somebody is waiting and nobody is playing yet. Zero players and a non-zero count here is exactly "a hibernated server has demand", which is the state Playpute will act on.
Who, capped — see ListPresenceResponse. Empty on a fleet-wide listing, where the counts are the point and the names would be the response.
display_namestringDisplay name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.
player_idstringStable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.
id_typestringWhich namespace player_id belongs to, e.g. "steam", "minecraft", "eos".
Two games can both log a 17-digit number and mean different things.
addressstringSource address, when logged.
A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.
The most recent thing this service observed about the server: a join, a part, a connection either way.
The field an idle-shutdown policy should read, and deliberately not "last_session_end": a server with a player who joined an hour ago and has said nothing since is busy, and one whose last event was a disconnect two hours ago is not.
has_provisional_sessionsboolOutput onlyWhether any active session came from a template still being shadowed. A
caller about to act on player_count — bill it, or keep a server alive on
the strength of it — should refuse when this is true.
Why a connection ended. Reported by the proxy, which is the only component that watched it close.
| Value | Description |
|---|---|
| CLOSE_REASON_UNSPECIFIED | |
| CLOSE_REASON_CLIENT_CLOSED | Ordinary: one side closed and the splice ended. |
| CLOSE_REASON_SERVER_CLOSED | |
| CLOSE_REASON_WAKE_TIMEOUT | The server never came up inside the proxy's wake timeout, so a player who was waiting was let go. The one close reason that is a product failure rather than somebody leaving. |
| CLOSE_REASON_UPSTREAM_UNREACHABLE | The proxy could not reach the server it was told to forward to. |
| CLOSE_REASON_EXPIRED | Nobody reported a close and the record aged out. See PlayerSession's END_REASON_EXPIRED, which carries the same warning: this is an upper bound on when it ended, not a measurement of it. |
| Value | Description |
|---|---|
| STATE_UNSPECIFIED | |
| STATE_OPEN | The socket is open. For a hibernated server this is a player waiting on an accepted connection that has not been forwarded anywhere yet. |
| STATE_CLOSED | The socket is closed, either end. |
How a session ended, which is not decoration: three of these five mean "nobody told us it ended and we inferred it", and a consumer that treats them as equivalent to a clean disconnect is one that bills for time nobody played.
| Value | Description |
|---|---|
| END_REASON_UNSPECIFIED | |
| END_REASON_DISCONNECTED | The server announced the player left. The only reason whose `end_time` is something the platform observed rather than inferred. |
| END_REASON_SUPERSEDED | The same player joined again with no leave in between, so the earlier session's end was never printed — a crash on the client, a lost line, or a game that does not announce every part. Ended at the later join. |
| END_REASON_SERVER_STOPPED | The server stopped, which ends every session on it whether or not anybody's departure was logged. A server being torn down is the single most common way a session really ends on this platform, because that is what happens when the last player leaves. |
| END_REASON_SERVER_RESTARTED | The server came back up. Anything still open belonged to the run before it and was never closed — a crash, an OOM kill, a host that went away. |
| END_REASON_EXPIRED | Nothing closed it and it outlived the maximum session age. `end_time` here is a **ceiling, not a measurement**: it says the session had ended by then, not that it ended then. Billing must clamp rather than charge it, and a support answer built on one should say so. |
| Value | Description |
|---|---|
| STATE_UNSPECIFIED | |
| STATE_ACTIVE | The player is believed to be on the server now. |
| STATE_ENDED |