SUNC HTTP & WebSocket
Roblox provides its own HTTP API (game:HttpGet, HttpService:RequestAsync), but it's rate- limited and lacks several headers that custom integrations need. SUNC's request function and WebSocket.connect bridge the gap with a richer, script-controlled API. This article walks through both.
request: the workhorse HTTP function
One function call, full control over method, URL, headers, and body. Returns a response object with status, body, and reply headers.
request(options: {
Url: string,
Method: string?, -- defaults to "GET"
Headers: { [string]: string }?,
Body: string?, -- for POST/PUT/PATCH
Cookies: { [string]: string }?,
}): {
Success: boolean,
StatusCode: number,
StatusMessage: string,
Headers: { [string]: string },
Cookies: { [string]: string },
Body: string,
}tablelocal response = request({
Url = "https://httpbin.org/post",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
["X-Client"] = "atherhub",
},
Body = game:GetService("HttpService"):JSONEncode({
version = "1.0",
nonce = math.random(),
}),
})
print(response.StatusCode) --> 200
print(response.Body) --> {"args":{},"data":"{...}","headers":...}Notice the response is synchronous from the script's point of view but actually executes off-thread internally — the call yields the script's coroutine until the response arrives. That means you can write linear request/response code without manual callbacks, but it also means you can't callrequest from a parallel context. Synchronise first.
Request idioms
A few patterns are worth knowing.
JSON over HTTP. The 90% case: encode a table to JSON, send as POST body, decode the response.
local HttpService = game:GetService("HttpService")
local function postJson(url: string, payload: any)
local response = request({
Url = url,
Method = "POST",
Headers = { ["Content-Type"] = "application/json" },
Body = HttpService:JSONEncode(payload),
})
if not response.Success then
warn("request failed:", response.StatusCode, response.StatusMessage)
return nil
end
local ok, data = pcall(HttpService.JSONDecode, HttpService, response.Body)
return ok and data or nil
endCustom user agents. By default SUNC sets a generic UA string. Some servers want you to identify yourself; request lets you set your own.
request({
Url = "https://api.example.com/me",
Headers = {
["User-Agent"] = "MyScript/1.0",
["Authorization"] = "Bearer " .. token,
},
})Binary downloads. The body is a raw byte string. Pair with writefile to persist binaries — images, audio, archives.
local response = request({ Url = "https://example.com/logo.png" })
if response.Success then writefile("logo.png", response.Body) endWebSocket.connect
For long-lived, bidirectional connections, request is the wrong shape. WebSocket.connect opens a standard WS or WSS connection and returns an object with events and methods.
WebSocket.connect(url: string): WebSocketThe returned WebSocket exposes:
:Send(data: string)— send a frame.:Close()— close the connection..OnMessage: RBXScriptSignal— fires once per inbound frame..OnClose: RBXScriptSignal— fires when the connection is closed by either side.
local ws = WebSocket.connect("wss://ws.example.com/")
ws.OnMessage:Connect(function(payload)
print("server →", payload)
end)
ws.OnClose:Connect(function()
print("connection closed")
end)
ws:Send('{"type":"hello"}')
task.wait(60)
ws:Close()WebSocketWebSocket idioms
A reconnecting client is the most common shape. WS connections drop for all kinds of reasons; long-running scripts should handle that automatically.
local function connect(url: string, onMessage: (string) -> ())
local ws
local function loop()
while true do
ws = WebSocket.connect(url)
ws.OnMessage:Connect(onMessage)
ws.OnClose:Wait() -- yield until the connection drops
task.wait(2) -- backoff before reconnect
end
end
task.spawn(loop)
return function() if ws then ws:Close() end end
end
local stop = connect("wss://ws.example.com/feed", function(msg)
print("update:", msg)
end)The pattern: open, listen, wait for close, sleep, reopen. The returned stop function gives the caller a clean way to tear down on script exit.
queue_on_teleport
When a Roblox game teleports the player to another place, all scripts (including yours) are killed. queue_on_teleport lets you stash a chunk of Luau that will auto-execute on the next loaded place. That's how scripts survive teleports.
queue_on_teleport(code: string): ()voidqueue_on_teleport([[
-- The same loader we ran on the first place
loadstring(game:HttpGet("https://example.com/loader.lua"))()
]])The string you queue is plain Luau source, not a function. It gets loadstring'd and run in the next place's script context. Keep it minimal — usually just a re-fetch of the main loader.
Error handling
Both request and WebSocket.connect will throw on entirely unreachable hosts (DNS failure, TLS handshake failure). Always wrap in pcall for anything you care about, and inspect StatusCode for the HTTP-level case.
local ok, response = pcall(request, { Url = "https://offline.invalid/" })
if not ok then
warn("network call threw:", response)
return
end
if not response.Success then
warn("HTTP error:", response.StatusCode)
return
endStatusCode first.Wrap-up
The network API is what unlocks most non-trivial script features: telemetry, remote config, live updates, content downloads. request is the workhorse, WebSocket is the long-lived sibling, and queue_on_teleport is the bridge between place sessions. None of these is complicated in isolation, but their combinations cover almost every reason a Roblox script needs to talk to the outside world.
Ather is the lead developer behind Atherhub. He's been writing Luau and Roblox tooling for the better part of a decade, with a focus on the messy interface between game-script internals and the platforms that host them. Have feedback on this article? Drop it in the Discord.