Roblox HttpService: External APIs, Limits, and Secrets
Everything your game touches inside Roblox has a dedicated service. Everything outside it goes through one: HttpService. Here is how to enable it, why RequestAsync is the method that matters, what the 500-requests-a-minute budget actually covers, and how the secrets store keeps your API keys out of your scripts.

Every other service your game leans on stays inside Roblox. DataStores persist to Roblox's cloud, memory stores and MessagingService connect your own servers to each other, remotes cross the client-server boundary. The moment you want anything beyond that wall — an analytics platform, an error logger, a translation API, your own backend — there is exactly one door, and it is HttpService.
The Creator Documentation lists the intended use cases plainly: "analytics, data storage, remote server configuration, error reporting, advanced calculations, or real-time communication," plus a subset of Roblox's own Open Cloud APIs. This guide covers the whole surface — the two different enable switches, the request method worth using, the real rate limits, the JSON utilities, and the secrets store that exists so your API key never sits in a script.
Server-only, and what it is actually for
The first sentence of the class reference sets the scope: HttpService "allows HTTP requests to be sent from experience servers." Roblox's own code samples put it even more bluntly: "HttpService cannot be used by LocalScripts."
So the shape of every integration is fixed before you write a line. A Script on the server makes the request; if a player's screen needs the result, the server hands it over through a RemoteEvent. There is no client-side fetch, which is also why exploiters cannot watch or replay your API traffic from inside the client.

The reference closes its overview with a warning worth keeping: "Only send HTTP requests to trusted third-party platforms to avoid introducing unnecessary security risks to your experience." Your game server is inside your security boundary; every endpoint you call is not.
Two switches: published places and the command bar
Requests are off by default, and there are two different switches depending on the state of your place:
- Published experience: enable Allow HTTP Requests under File ⟩ Experience Settings ⟩ Security in Studio.
- Unpublished place: the settings toggle is not available yet, so the docs say to flip the property from the Command Bar:
game:GetService("HttpService").HttpEnabled = true.
That second path is deliberately manual. HttpEnabled carries LocalUserSecurity on write, so a regular Script cannot switch it on at runtime — a place either ships with HTTP enabled or it does not.
Plugins get their own arrangement: a plugin may use HttpService, and the first time it tries, Studio can prompt the user to grant that plugin permission for the specific web address. Those grants are managed — accepted, denied, revoked — in the Plugin Management window. Plugins can also talk to localhost and 127.0.0.1, which is how Studio tooling communicates with software running on the same machine.
RequestAsync is the real method
Three methods send requests: RequestAsync(), GetAsync() and PostAsync(). The docs are unambiguous about the hierarchy — GetAsync() and PostAsync() are each described as "useful only as a shorthand," and RequestAsync() "should be used in most cases." The shorthands return only the response body; RequestAsync() returns the whole response, which you need the moment anything goes wrong.
RequestAsync() takes a single dictionary:
| Field | Type | Required | What it does |
|---|---|---|---|
Url | string | yes | Target URL — http or https only |
Method | string | no | One of eight verbs: GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH |
Headers | dictionary | no | Custom headers — most are accepted, not all |
Body | string | no | Any string, including binary. Must be excluded on GET and HEAD |
Compress | Enum.HttpCompression | no | None or Gzip |
Timeout | integer | no | Seconds; must be greater than zero and no greater than the default request timeout |
Three header rules from the reference save real debugging time. Content-Length is computed from your body — you cannot set it. User-Agent and Roblox-Id "are locked by Roblox," so your script cannot disguise the request as coming from anything other than a Roblox game server. And RequestAsync() "does not detect the format of body content" — if you send JSON, set Content-Type: application/json yourself, because unlike PostAsync() there is no content-type enum doing it for you. The recognized values are text/plain, text/xml, application/xml, application/json and application/x-www-form-urlencoded.
A minimal POST, shaped the way the docs shape theirs:
local HttpService = game:GetService("HttpService")
local function reportEvent(payload)
local response = HttpService:RequestAsync({
Url = "https://my.example.com/events",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode(payload),
})
return response
end
local ok, result = pcall(reportEvent, { event = "match_start", mode = "ranked" })
if not ok then
warn("Request failed to send:", result)
end

Reading the response, and handling the failures
RequestAsync() returns a dictionary:
| Field | Type | Meaning |
|---|---|---|
Success | boolean | True if and only if StatusCode is in the range 200–299 |
StatusCode | integer | The HTTP response code |
StatusMessage | string | The status message sent back |
Headers | dictionary | Response headers |
Body | string | The response content |
Failure lives on two separate layers. A 404 or a 500 is a completed request — RequestAsync() returns normally and Success is false. The method only raises an error "if the response times out or if the target server rejects the request." So you need both checks: pcall() around the call for the transport layer, then a Success check on the result for the HTTP layer. The docs' own framing for why: "If a web service goes down for some reason, it can cause scripts that use this method to stop functioning altogether."
When a request fails with something recoverable, the best-practices section prescribes exponential backoff: wait two seconds, then four, then eight between attempts, giving the endpoint room to recover instead of hammering it. And whatever comes back, "strictly validate and sanitize all received data from external APIs" — an external response is client input with better manners.
The budget: 500 a minute, plus a separate Open Cloud lane
Here are the rate limits as the current docs state them:
| Limit | Value |
|---|---|
| External HTTP requests | 500 per minute — "Requests over these thresholds will fail" |
| Open Cloud requests | 2,500 per minute per game server, a separate allowance |
| Exceeding the Open Cloud limit | Request methods can stall for around 30 seconds; pcall can fail with "Number of Open Cloud requests exceeded limit" |
| Blocked ports | Port 1194 and every port below 1024 except 80 and 443 — a blocked port returns 403 Forbidden or ERR_ACCESS_DENIED |
The line that matters for architecture: Open Cloud requests "do not consume the same overall limit of 500 HTTP requests per minute enforced on all other requests." Talking to Roblox's own APIs rides a wider, separate lane than talking to the outside world.
Inside the 500, the guide's advice is to aggregate: if you are about to send one request per player, check whether the API has a bulk endpoint and send one request for all of them. HttpService also "automatically uses HTTP/2 when available" — with the footnote that the HTTP/2 specification requires header names in lowercase.
Calling Roblox itself: the Open Cloud subset
HttpService can call a subset of Open Cloud endpoints directly — the docs list families covering assets, bans and blocks, configs, the Creator Store, developer products, game passes, data stores, memory stores, ordered data stores, groups, inventories, Luau execution, notifications, places, universes and users. That list includes some genuinely powerful verbs: PublishUniverseMessage, RestartUniverseServers, UpdateUserRestriction for bans.
The flow is three steps: create an Open Cloud API key, save it to your experience's secrets store, then call the endpoint like any other URL with the key attached:
local HttpService = game:GetService("HttpService")
local response = HttpService:RequestAsync({
Url = "https://apis.roblox.com/cloud/v2/universes/YOUR_UNIVERSE_ID",
Method = "GET",
Headers = {
["x-api-key"] = HttpService:GetSecret("APIKey"),
},
})
The restrictions on this lane are strict, and they are quoted here exactly because each one is a distinct failure mode: "Only the x-api-key and content-type headers are allowed." The x-api-key header "must be a Secret" — a raw string key is rejected. The .. string "is not allowed in URL path parameters" to Roblox domains, which means data store entries containing .. are currently unreachable this way. And only HTTPS is supported. On top of the per-server 2,500, each endpoint also has its own limit per API key owner, enforced no matter where the calls come from.
The utilities that work with HTTP off
Four methods on HttpService never touch the network, and the docs note each "can be used regardless of whether HTTP requests are enabled":
JSONEncode(input)turns a Luau table into JSON. The sharp edges: a table with both string and number keys encodes as an array — "an array takes priority (string keys are ignored)"; an empty table{}becomes an empty JSON array, not an object;nilvalues in a sequence should be avoided; cyclic references throw. It will happily emitinfandnan, "which are not valid JSON" — fine inside Roblox, a parse error in someone else's stack. It also accepts buffers up to 50 MiB, encoding them to base64.JSONDecode(input)reverses it, and throws on invalid JSON — one more reason these calls sit insidepcall. A JSON object holding both string and numeric keys keeps the numbers; string keys are ignored.GenerateGUID(wrapInCurlyBraces)returns a random version-4 UUID — 32 hex digits in the 8-4-4-4-12 pattern, 36 characters. The parameter defaults totrue, so trim expectations accordingly:{94b717b2-...}with braces, plain without.UrlEncode(input)percent-encodes a string for use in URLs orapplication/x-www-form-urlencodedbodies.
Secrets: stop pasting API keys into scripts
Roblox gives every experience a secrets store, and the docs name the anti-pattern it replaces: "You could copy and paste the API key into a script or add it to a data store, but those approaches carry unnecessary security risks."
The store's shape: up to 500 secrets per experience, each up to 1,024 characters, created under Secrets ⟩ Create Secret on the Creator Dashboard, manageable only by the experience owner or group owner. Each secret is scoped to a domain, with limited wildcard support — * for any domain (which the docs recommend against), *.example.com for subdomains, or best of all one specific host.
In a server script, HttpService:GetSecret("name") returns not a string but a Secret — a value you cannot read. Print it and you get Secret(name). The only manipulations are AddPrefix() and AddSuffix(), which exist so you can build a URL or an Authorization header around a value your code never sees. One hard boundary: secrets can go in the URL and headers, but "You can't include secrets in the HTTP request body."
Two testing behaviors will bite you exactly once. The store is only available "to live servers or collaborative testing environments" — during local playtesting, GetSecret() fails with Can't find secret with given key, and a client script gets the identical error. For local work, define stand-in values in the Local Secrets section under File ⟩ Experience Settings ⟩ Security.
CreateWebStreamClient is Studio-only
The newest addition to the class handles what plain requests cannot: server-pushed data. CreateWebStreamClient() opens a long-lived connection to endpoints using SSE, chunked transfer encoding, or WebSockets, firing signals you connect callbacks to as data arrives.
Read the constraint before designing around it: "This method is available in Studio only. If you use it inside scripts, make sure to remove any references before publishing the experience." The docs pitch it for plugins — live services keep using request-response within the budgets above. There is a cap of six clients at a time, streams should be shut with WebStreamClient:Close(), and lingering RBXScriptConnections are called out as a memory leak source.
Watching it live
HttpService has its own Observability Dashboard with two charts: Request Count, tracking the volume of requests from your game, and Response Time, measuring endpoint latency. You can filter and break both down by request type (GET, POST, PUT, PATCH, DELETE, Other) and by status — where the buckets are Success (1xx/2xx), Redirect (3xx), the specific codes 400, 401, 403, 404, 429, 500 and 503, plus ExternalError for anything else the external service returns and InternalError for issues raised inside Roblox itself. One dashboard quirk the docs flag: the Response Time chart is not correlated with status data, so selecting a Status breakdown blanks it.
The guide's closing security note deserves the last word: your endpoints should require "a secure form of authentication, such as a pre-shared secret key, so that bad actors cannot pose as one of your Roblox servers." The requests leave Roblox with a locked User-Agent, but anyone can imitate a request shape — the secret in the header is what proves it came from you.
Quick Action Checklist
- Enable Allow HTTP Requests under File ⟩ Experience Settings ⟩ Security; for an unpublished place, set
HttpEnabled = truefrom the Command Bar. - Keep every call in a server
Script— "HttpService cannot be used by LocalScripts" — and relay results to clients over remotes. - Use
RequestAsync()and setContent-Typeyourself when sending JSON; treatGetAsync()/PostAsync()as the shorthands the docs say they are. - Handle both failure layers:
pcall()for timeouts and rejected connections, then checkSuccess(true only for 200–299) on the returned dictionary. - Budget against 500 external requests per minute; batch per-player calls into bulk requests where the API allows.
- Route Roblox-API work through the Open Cloud lane — 2,500 per minute per game server, separate from the 500 — with
x-api-keysupplied as aSecret. - Avoid ports below 1024 other than 80 and 443, and port 1194 — blocked ports fail with
403 ForbiddenorERR_ACCESS_DENIED. - Put API keys in the secrets store, scope each to a specific domain, and remember secrets work in URLs and headers but never the body.
- Back off exponentially on recoverable errors — two seconds, then four, then eight.
- Validate everything an external API returns as strictly as you validate client input.
Frequently Asked Questions
Keep Reading
Related Guides

Roblox Cross-Server Data: Making 200 Servers Act Like One Game
Your game is not one world — it is however many servers Roblox spun up, each blind to the others. Global leaderboards, shared auctions, cross-server announcements and matchmaking all run through two services: MemoryStoreService and MessagingService. Here is what they actually cost you, and where the real ceilings sit.

Roblox Instance Streaming: The Setting That Quietly Breaks Your Client Scripts
StreamingEnabled is on by default for every new place you make in Studio, and it rewrites the contract your client scripts were built on. workspace.House.Door is no longer a thing that exists — it's a thing that might exist, depending on where the player is standing. Here is what the engine actually does, which of the eight Workspace properties matter, and the five script patterns that break the moment streaming is live.

Roblox Heartbeat vs RenderStepped vs Stepped: Which Loop Should Actually Run Your Code
Every Roblox tutorial teaches the same loop — while true do wait() end — and every one of them is teaching you a bug. Roblox gives you nine documented points per frame where a script can wake up, and picking the wrong one is why your camera jitters, your physics reads a frame late, and your Motor6D edits get stomped by the Animator. Here is what each RunService event actually does, why the old names still work despite a 2021 deprecation scare, and the 29-millisecond tax the legacy wait() charges you.

Roblox Memory Leaks: Why Your Game Gets Laggier the Longer a Server Lives
Your game runs great for the first ten minutes and turns to soup by minute forty — same map, same player count, nothing added. That is a memory leak, and on Roblox it is almost always event connections your code never disconnected. Here is how to measure it with the Developer Console and the Stats service, the four leaks that cause 90% of it, and a cleanup pattern that stops the bleeding.

Best Roblox Games to Play in 2026
Roblox's front page is engagement bait. This is the filtered version: the games with real, sustained player counts and actual staying power, sorted by what you're in the mood for.

How to Get Robux Safely (Legit Ways + Scams to Avoid)
There is no free Robux generator. There never was. Here are the actual legit ways to get Robux without overpaying, the earning methods that really work, and the scams that exist purely to steal your account.