This tutorial covers on how to make a Public & Private Server Browser, meaning:
- Public Server Browser: Players can view all public servers and choose which one to join
- Private Server Browser: Players can create & join private servers, viewable by everyone
This tutorial only covers the basics, more advanced features for private servers such as invite-only, ban list, creator control over the server, etc have to be made by you.
NOTE: This is ONLY a tutorial, you may use pieces of the tutorial’s code, however some parts are in blank for you to fill in using your game’s systems or systems you have to create yourself (such as notifying a user when a problem occurs and adding a filter to server name)
There may be mistakes in the code or hard-to-read sections, if you stumble upon one of them, please comment in this post
This tutorial uses @prooheckcp’s CountryService module to get the flag decal of certain countries
Making a Public Server Browser
Setup
For this tutorial, this will be the UI (you can make your own), which should be enough:
Next, put @prooheckcp’s CountryService module somewhere, in this tutorial I will place it in ReplicatedStorage
At last, add 1 Remote Event and 1 Remote Function to ReplicatedStorage, you can name them whatever you want or place them inside folders if you want.
Server Script
Now we get to the Server Script, this will handle the server’s communication to the client and also add the current server to the server list (if it’s a public server). To start, add the variables:
--Variables
local MemoryStoreService = game:GetService("MemoryStoreService")
local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")
local Remote = game.ReplicatedStorage.RemoteFunction --Make sure this is the one in your game!
local Event = game.ReplicatedStorage.RemoteEvent --Make sure this is the one in your game!
local SortedMap = MemoryStoreService:GetSortedMap("PublicServerList")
local PlayerCooldowns = {}
We will also need this helper function:
function RetryRequest(Object:Instance, Function:string, ...)
local MaxRetries = 5 --Change this to whatever you prefer
local Cooldown = 1
local Tries = 0
local ErrorMessage = "Unknown Error"
while Tries <= MaxRetries do
Tries += 1
local Result = {pcall(Object[Function], Object, ...)}
if Result[1] then
--Request resulted in success!
return unpack(Result)
else
ErrorMessage = Result[2] or ErrorMessage
end
task.wait(Cooldown)
end
return false, ErrorMessage
end
Now we start with the server logic, we need to store a variable ‘ServerList’ for later use, while updating it every once in a while, and when we update it, we also let all the clients know:
local ServerList = {}
task.spawn(function()
while true do
local Success, NewServerList = RetryRequest(SortedMap, "GetRangeAsync", Enum.SortDirection.Ascending, 50)
if Success then
ServerList = NewServerList
Event:FireAllClients("NewServerList", NewServerList)
else
warn("Failed to get server list: "..NewServerList)
end
task.wait(10) --Update the server list every 10 seconds
end
end)
Next we need to handle the client to server communication, this will handle requests when the client wants to get the server list, quick join and join a specific server, while also applying rate-limits for safety:
Remote.OnServerInvoke = function(Player:Player, Action, Data)
local UserId = Player.UserId
--Rate-limit the player to only allow requests every 5 seconds
local LastRequest = PlayerCooldowns[UserId] or 0
local Now = tick()
if Now - LastRequest < 5 then
return false, "Rate-limited"
end
PlayerCooldowns[UserId] = Now
if Action == "GetServerList" then
return true, ServerList
elseif Action == "JoinServer" then
local JobId = Data
--Verify if the server is in the server list
for _, Server in ServerList do
if Server.key == JobId then
--Server is indeed in the list!
local Options = Instance.new("TeleportOptions")
Options.ServerInstanceId = JobId
RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player}, Options)
return true
end
end
return false, "Server not found"
elseif Action == "QuickJoin" then
RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player})
return true
end
end
We are almost done! Now, if the current server is a public server, we need to add it to the server list. So let’s start with this check:
if game.PrivateServerId ~= "" then
return --End script here if it's a private/VIP server
end
Then let’s get the country and region of the server, we will use this later to let players know where each server is located:
--Get the server location
local Country = game.LocalizationService.SystemLocaleId:sub(4,5):upper() or "??"
local Region = "Unknown"
local City = "Unknown"
local HTTP = game:GetService("HttpService")
local Success, Body = RetryRequest(HTTP, "GetAsync", "http://ip-api.com/json/")
if Success then
Body = HTTP:JSONDecode(Body)
Country = Body.countryCode or Country
Region = Body.regionName or Region
City = Body.city or City
end
--If HTTPService is disabled or the HTTP request fails, Region and City will default to "Unknown"
```
Then let's set the JobId variable:
```
local JobId = game.JobId
if game:GetService("RunService"):IsStudio() then
JobId = tostring(math.random(1000,100000000))
--Studio doesn't have JobId
end
```
Now that we have everything necessary, we need to make the function that adds the serverdata (Location data, PlayerList, etc) to the serverlist in MemoryStorageService:
```
--Function to add the server to the server list
local function Update ()
local PlayerList = {}
for _, Player in Players:GetPlayers() do
table.insert(PlayerList, Player.UserId)
end
local Success, Message = RetryRequest(SortedMap, "SetAsync", JobId, {
Country = Country,
Region = Region,
City = City,
Age = time(),
Players = PlayerList
}, 20)
if not Success then
warn("Failed to update server: "..Message)
end
return Success
end
```
Everytime we run the function Update(), the current server is added/updated to the serverlist for 20 more seconds, next we need to add the loop that is actually gonna keep updating the server so that it doesn't expire and disappear from the serverlist, then when the server shuts down we remove it from the server list so that it doesn't stay there as a ghost server for 20 seconds:
```
local Closing = false
--Loop to update the server constantly
task.spawn(function()
while not Closing do
Update()
task.wait(10)
end
end)
--When the server shuts down...
game:BindToClose(function()
Closing = true
RetryRequest(SortedMap, "RemoveAsync", JobId) --Remove from server list
end)
```
The server script is finished! Next, we need to do the client-side logic
Local Script
This ClientScript will be placed in: UI (ScreenGui) → MainFrame → HERE
First set the variables:
--Variables
local Players = game:GetService("Players")
local Replicated = game.ReplicatedStorage
local CountryService = require(Replicated:WaitForChild("CountryService"))
local Remote = Replicated:WaitForChild("RemoteFunction")
local Event = Replicated:WaitForChild("RemoteEvent")
local MainFrame = script.Parent
local UI = MainFrame.Parent
local Close = MainFrame.Close
local QuickJoin = MainFrame.QuickJoin
local List = MainFrame.List
local Template = List.Template
Then make the code that destroys (or disables) the UI when the X button gets clicked:
--Close button
Close.MouseButton1Click:Connect(function()
UI:Destroy()
end)
Then the code for the quick join button, quick joining will just throw the player into a random public server that roblox decides it’s best:
--Quick join button
QuickJoin.MouseButton1Click:Connect(function()
local Success, Msg = Remote:InvokeServer("QuickJoin")
if not Success then
return warn("Client failed to quick join: "..Msg)
end
end)
Now we will make a function that creates each server in the server list as an UI element every time it gets refreshed:
--Refresh the server list
local function Refresh(ServerList)
--Get the server list from the server
if not ServerList then
local Success, Received = Remote:InvokeServer("GetServerList")
if not Success then
return warn("Client failed to get server list: "..Received)
end
ServerList = Received
end
--Delete existing items in the server list
for _,Object in List:GetChildren() do
if Object:IsA("Frame") and Object.Name ~= "Template" then
Object:Destroy()
end
end
--Add the new servers in the server list
for _, Item in ServerList do
local JobId = Item.key
local ServerData = Item.value
local Frame = Template:Clone()
Frame.Name = JobId
--Get country image
local CountryData = CountryService:GetCountryByCode(ServerData.Country)
if CountryData then
Frame.Country.Image = CountryData.Decal or ""
end
Frame.Location.Text = ServerData.Region..", "..ServerData.City
--Create the player icons
for i, UserId in ServerData.Players do
local ImgLabel = Frame.PlayerList.Template:Clone()
ImgLabel.Image = Players:GetUserThumbnailAsync(UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size48x48)
ImgLabel.Visible = true
ImgLabel.Name = UserId
ImgLabel.Parent = Frame.PlayerList
--Playerlist frame can only fit 8 items, if this is the seventh player then add a +n:
if i == 7 and #ServerData.Players > 7 then
local More = Frame.PlayerList.More:Clone()
More.Text = "+"..(#ServerData.Players - 7)
More.Parent = Frame.PlayerList
More.Visible = true
end
end
--Joining the server
Frame.Join.MouseButton1Click:Connect(function()
local Success, Msg = Remote:InvokeServer("JoinServer", JobId)
if not Success then
warn("Client failed to join server: "..Msg)
end
end)
Frame.Visible = true
Frame.Parent = List
end
return true
end
After that function, call it so that the serverlist is already made the moment the script runs:
Refresh()
Lastly, whenever we receive a new server list from the server, we need to refresh it in the client’s UI again:
--Event received from the server...
Event.OnClientEvent:Connect(function(Action, Data)
if Action == "NewServerList" then
Refresh(Data)
end
end)
Place File for the Public Server Browser: PublicServerBrowser.rbxl (101.7 KB)
Making a Private Server Browser
Note: Since this is just a tutorial, it doesn’t have systems such as setting your private server to Invite-Only, banning players from your server and etc. You have to make those yourself using your game’s existing systems or a new system
Setup
For this tutorial, this will be the UI (you can make your own), which should be enough:
At last, add 1 Remote Event and 1 Remote Function to ReplicatedStorage, you can name them whatever you want or place them inside folders if you want.
Server Script
Now we get to the Server Script, this will handle the server’s communication to the client and also add the current server to the server list (if it’s a valid private server). To start, add the variables:
--Variables
local MemoryStoreService = game:GetService("MemoryStoreService")
local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")
local Remote = game.ReplicatedStorage.RemoteFunction
local Event = game.ReplicatedStorage.RemoteEvent
local SortedMap = MemoryStoreService:GetSortedMap("PrivateServerList")
local PlayerCooldowns = {}
We will also need this helper function:
function RetryRequest(Object:Instance, Function:string, ...)
local MaxRetries = 5 --Change this to whatever you prefer
local Cooldown = 1
local Tries = 0
local ErrorMessage = "Unknown Error"
while Tries <= MaxRetries do
Tries += 1
local Result = {pcall(Object[Function], Object, ...)}
if Result[1] then
--Request resulted in success!
return unpack(Result)
else
ErrorMessage = Result[2] or ErrorMessage
end
task.wait(Cooldown)
end
return false, ErrorMessage
end
Now we start with the server logic, we need to store a variable ‘ServerList’ for later use, while updating it every once in a while, and when we update it, we also let all the clients know:
local ServerList = {}
task.spawn(function()
while true do
local Success, NewServerList = RetryRequest(SortedMap, "GetRangeAsync", Enum.SortDirection.Ascending, 50)
if Success then
ServerList = NewServerList
Event:FireAllClients("NewServerList", NewServerList)
else
warn("Failed to get server list: "..NewServerList)
end
task.wait(10) --Update the server list every 10 seconds
end
end)
Next we need to handle the client to server communication, this will handle requests when the client wants to get the server list, join a specific server and create a server, while also applying rate-limits for safety:
--Connection between the client and server
Remote.OnServerInvoke = function(Player:Player, Action, Data)
local UserId = Player.UserId
--Rate-limit the player to only allow requests every 5 seconds
local LastRequest = PlayerCooldowns[UserId] or 0
local Now = tick()
if Now - LastRequest < 5 then
return false, "Rate-limited"
end
PlayerCooldowns[UserId] = Now
if Action == "GetServerList" then
return true, ServerList
elseif Action == "JoinServer" then
local JobId = Data
if game.JobId == JobId then
return false, "You're already in this server!"
end
--Verify if the server is in the server list
for _, Server in ServerList do
if Server.key == JobId then
--Server is indeed in the list!
local Options = Instance.new("TeleportOptions")
Options.ReservedServerAccessCode = Server.value.AccessCode
Options:SetTeleportData({
ServerName = Server.value.ServerName,
AccessCode = Server.value.AccessCode,
Creator = Server.value.Creator
})
RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player}, Options)
return true
end
end
return false, "Server not found"
elseif Action == "CreateServer" then
--Security checks
if not Data or type(Data) ~= "string" then
return false, "Invalid data"
end
if #Data == 0 then
return false, "Name too short"
end
if #Data > 50 then
return false, "Name too long"
end
--[[------WARNING----------
This code has no filtering for the server name.
This is only a tutorial so you must implement the filter yourself.
---------------------------]]
local Success, AccessCode = RetryRequest(TeleportService, "ReserveServer", game.PlaceId)
if not Success then
return false, AccessCode
end
local Options = Instance.new("TeleportOptions")
Options.ReservedServerAccessCode = AccessCode
Options:SetTeleportData({
ServerName = Data,
AccessCode = AccessCode,
Creator = UserId
})
RetryRequest(TeleportService, "TeleportAsync", game.PlaceId, {Player}, Options)
return true
end
end
Great! Now before we add the code that adds the current server to the server list, we need to add a check to see if the server currently running has been created by a player:
if game.PrivateServerId == "" or game.PrivateServerOwnerId ~= 0 then
return --Stop the script if we are in a public or VIP server
end
local Player = Players:FindFirstChildOfClass("Player") or Players.PlayerAdded:Wait()
local Data = Player:GetJoinData()
if not Data or not Data.TeleportData then
return --Stop the script if the player didn't join through a teleport
end
local ServerName = Data.TeleportData.ServerName
local AccessCode = Data.TeleportData.AccessCode
local Creator = Data.TeleportData.Creator
if not AccessCode or not ServerName or not Creator then
return --Stop the script if the teleport data is invalid
end
Now that we have added the check, we need to make the function that adds the current server’s data (Name, Creator, PlayerList, etc) to the serverlist in MemoryStorageService:
local JobId = game.JobId
--Function to add the server to the server list
local function Update ()
local PlayerList = {}
for _, Player in Players:GetPlayers() do
table.insert(PlayerList, Player.UserId)
end
local Success, Message = RetryRequest(SortedMap, "SetAsync", JobId, {
Age = time(),
Players = PlayerList,
ServerName = ServerName,
AccessCode = AccessCode,
Creator = Creator
}, 20)
if not Success then
warn("Failed to update server: "..Message)
end
return Success
end
Everytime we run the function Update(), the current server is added/updated to the serverlist for 20 more seconds, next we need to add the loop that is actually gonna keep updating the server so that it doesn’t expire and disappear from the serverlist, then when the server shuts down we remove it from the server list so that it doesn’t stay there as a ghost server for 20 seconds:
local Closing = false
--Loop to update the server constantly
task.spawn(function()
while not Closing do
Update()
task.wait(10)
end
end)
--When the server shuts down...
game:BindToClose(function()
Closing = true
RetryRequest(SortedMap, "RemoveAsync", JobId) --Remove from server list
end)
Local Script
This ClientScript will be placed in: UI (ScreenGui) → MainFrame → HERE
First set the variables:
--Variables
local Players = game:GetService("Players")
local Replicated = game.ReplicatedStorage
local Remote = Replicated:WaitForChild("RemoteFunction")
local Event = Replicated:WaitForChild("RemoteEvent")
local MainFrame = script.Parent
local UI = MainFrame.Parent
local Close = MainFrame.Close
local Create = MainFrame.Create
local NameInput = MainFrame.TextBox
local List = MainFrame.List
local Template = List.Template
Then the code for the close button that destroys (or disables) the UI:
--Close button
Close.MouseButton1Click:Connect(function()
UI:Destroy()
end)
Then the code for creating a server:
--Create button
Create.MouseButton1Click:Connect(function()
local Success, Msg = Remote:InvokeServer("CreateServer", NameInput.Text)
if not Success then
return warn("Client failed to create server: "..Msg)
end
end)
Then the code for limiting the inputted server name to 50 characters:
--Limit the name to 50 characters
NameInput:GetPropertyChangedSignal("Text"):Connect(function()
NameInput.Text = NameInput.Text:sub(1, 50)
end)
Now we will make a function that creates each server in the server list as an UI element every time it gets refreshed:
--Refresh the server list
local function Refresh(ServerList)
--Get the server list from the server
if not ServerList then
local Success, Received = Remote:InvokeServer("GetServerList")
if not Success then
return warn("Client failed to get server list: "..Received)
end
ServerList = Received
end
--Delete existing items in the server list
for _,Object in List:GetChildren() do
if Object:IsA("Frame") and Object.Name ~= "Template" then
Object:Destroy()
end
end
--Add the new servers in the server list
for _, Item in ServerList do
local JobId = Item.key
local ServerData = Item.value
local Frame = Template:Clone()
Frame.Name = JobId
Frame.Profile.Image = Players:GetUserThumbnailAsync(ServerData.Creator, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size60x60)
Frame.Title.Text = ServerData.ServerName
--Create the player icons
for i, UserId in ServerData.Players do
local ImgLabel = Frame.PlayerList.Template:Clone()
ImgLabel.Image = Players:GetUserThumbnailAsync(UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size48x48)
ImgLabel.Visible = true
ImgLabel.Name = UserId
ImgLabel.Parent = Frame.PlayerList
--Playerlist frame can only fit 8 items, if this is the seventh player then add a +n:
if i == 8 and #ServerData.Players > 8 then
local More = Frame.PlayerList.More:Clone()
More.Text = "+"..(#ServerData.Players - 8)
More.Parent = Frame.PlayerList
More.Visible = true
end
end
--Joining the server
Frame.Join.MouseButton1Click:Connect(function()
local Success, Msg = Remote:InvokeServer("JoinServer", JobId)
if not Success then
warn("Client failed to join server: "..Msg)
end
end)
Frame.Visible = true
Frame.Parent = List
end
return true
end
After that function, call it so that the serverlist is already made the moment the script runs:
Refresh()
Lastly, whenever we receive a new server list from the server, we need to refresh it in the client’s UI again:
--Event received from the server...
Event.OnClientEvent:Connect(function(Action, Data)
if Action == "NewServerList" then
Refresh(Data)
end
end)
Place File for the Private Server Browser:
PrivateServerBrowser.rbxl (98.1 KB)
Note: Since the Places above are empty baseplates, the client loads instantly before the server can add itself to the serverlist and show in the UI, that’s why it takes 10 seconds to show in the server list



