article · 2026-02-05
Robust HTTP in UE5: Timeouts, Retries, and Connection Error Handling
How to set per-request timeouts, retry only the failures worth retrying, and budget the worst case so your shipped game never hangs on a dead endpoint.
Why network calls fail in shipped games
On your development machine, every request to your backend returns in milliseconds and you almost never see a failure. In a shipped game it is the opposite. Players are on flaky mobile hotspots, hotel wifi behind a captive portal, corporate proxies that swallow connections, and home routers that drop the link mid-handshake. Your leaderboard call, your cloud-save fetch, your live config pull at Begin Play, all of them will eventually hit a connection that simply never answers.
The two failure modes you must design for are distinct. A connection error means the request never reached a server or never got a reply: DNS could not resolve, the socket timed out, the TLS handshake failed. An HTTP error response is the opposite, the server did reply, it just said no, with a status code in the 4xx or 5xx range. Conflating these is the classic cause of an Unreal Engine HTTP request retry timeout connection error bug, where a game retries a request the server has already permanently rejected, or hangs forever on a host that will never respond.
EasyHTTP is built around this distinction. It is a Blueprint-friendly wrapper over the engine's HTTP module, exposing every common method (GET, POST, PUT, PATCH, DELETE) through async latent nodes with separate On Success, On Failure and On Progress execution pins. The interesting part for resilience is the FEasyHTTPRequestOptions struct, which carries TimeoutSeconds, bRetryOnConnectionError, RetryCount and RetryDelaySeconds, so you control failure behaviour per request rather than globally.
Setting TimeoutSeconds per request
A timeout is your hard guarantee that a request will not hang the calling logic forever. In EasyHTTP this lives on the request options as TimeoutSeconds, which defaults to 30 seconds and accepts a range from 0 to 300 (five minutes is the ceiling). The convenience nodes Async Quick GET and Async Quick POST JSON already default to a 30 second timeout, so you only need to think about this when you build options explicitly.
Pick the timeout to match the call's purpose, not a single house value. A login or live config pull at Begin Play that gates the player getting into the game wants a short, aggressive timeout, perhaps 5 to 10 seconds, so a dead backend fails fast and you can show a retry prompt rather than a frozen loading screen. A large upload driven by the On Progress (BytesSent) callback legitimately needs far longer, and is exactly the case the 300 second ceiling exists for.
To set it, build an FEasyHTTPRequestOptions from 'Make Default Options' (or one of the 'Make Options With...' auth builders), set the 'Timeout Seconds' field, and feed it into the 'Easy HTTP Request' node. The Options pin sits under Advanced Display, so expand the node's advanced view to reach it. Because the timeout is per request, a slow analytics ping and a snappy auth check can each carry the budget that suits them.
Retry on connection error, and why 4xx and 5xx do not retry
Retries are governed by three fields on the options struct. Set bRetryOnConnectionError to true, then RetryCount controls how many additional attempts are made (range 0 to 10) and RetryDelaySeconds is the pause between attempts (range 0.1 to 60). A RetryCount of 3 means up to four total attempts: the original plus three retries.
The critical design rule, and the one that prevents the most subtle bugs, is that EasyHTTP only retries on connection failures. It never retries a 4xx or 5xx HTTP error response. This is deliberate and correct. A 401 Unauthorized will be 401 on every attempt until you fix the token; a 404 will never become a 200; a 400 Bad Request means your payload is wrong, not that the network blinked. Hammering the server with retries for these wastes the player's bandwidth and your backend's capacity while changing nothing. A dropped connection, by contrast, is exactly the transient condition a retry is designed to ride out.
This means error handling splits cleanly into two channels. The On Failure pin and the retry machinery cover connection-level problems. For server-level rejections, let the request complete and inspect the FEasyHTTPResponse: check bSuccess, read StatusCode, and use the IsResponseSuccessful helper to branch. A 503 from an overloaded service, for example, is something you may choose to retry yourself with your own backoff after reading the status, because EasyHTTP will treat that completed-but-rejected response as a non-retryable outcome by design.
A sensible starting point for a player-facing call is bRetryOnConnectionError true, RetryCount 2 or 3, and RetryDelaySeconds around 1 to 2 seconds. That rides out a brief drop without making a stalled player wait through ten attempts.
Budgeting the worst case before you ship
Retries and timeouts interact, and if you do not do the arithmetic you can accidentally build a request that, in the worst case, blocks for minutes. The combined ceiling is given by a simple formula: worst-case total time = (TimeoutSeconds x (RetryCount + 1)) + (RetryDelaySeconds x RetryCount).
Work an example. With a 30 second timeout, RetryCount 3 and a 2 second retry delay, the worst case is (30 x 4) + (2 x 3) = 126 seconds. That is over two minutes of a player staring at a spinner if the backend is hard down. Now contrast a tuned player-facing call: an 8 second timeout, RetryCount 2 and a 1.5 second delay gives (8 x 3) + (1.5 x 2) = 27 seconds, a far more humane failure window.
Run this calculation for every networked call before you ship, and design your UI around the number it produces. The On Progress callback exposing BytesSent and BytesReceived lets you show real movement during the wait, and a visible cancel or retry-now option turns the worst case into something the player controls rather than endures. The response also reports ElapsedTimeSeconds, which you can log to see how your real-world timings compare against the budget you planned for.
Cut the calls you make in the first place
The most reliable network request is the one you never send. Before reaching for more aggressive retries, look at whether you are calling at all when you do not need to. Live config and remote settings pulled at Begin Play rarely change between sessions, so caching the last good response and only refreshing on an interval (or on an explicit user action) removes a whole class of startup-blocking failures.
EasyHTTP gives you what you need to cache cleanly. Read Content (the body as a string) or RawContent (the bytes) from the FEasyHTTPResponse, persist it, and serve from your cache when a fresh fetch fails its retries. Pair this with the response StatusCode and Headers so you can respect cache-control semantics from your own backend. Fewer live calls means fewer chances for a connection error to surface in front of the player at all.
During development, you do not need a real backend to exercise any of this. EasyHTTP ships a built-in local test server (default port 8080): call 'Start Local Test Server', point requests at http://localhost:8080/, script specific responses with 'Set Server Response', inspect what your game actually sent with 'Get Last Server Request', and call 'Stop All Test Servers' on End Play. It is the cleanest way to deliberately provoke timeouts and connection errors and confirm your retry and fallback paths behave before a real player ever hits them. Note the test server is HTTP-only and intended for development, not production.
Two limits worth keeping in mind as you design: EasyHTTP buffers the full response (there is no streaming) and has no WebSocket support, so it is built for request and response REST traffic rather than long-lived connections. It runs on Windows 64-bit and HTTPS is handled through the engine's own SSL/TLS.
Connection error vs HTTP error response
| Aspect | Connection error | HTTP error response (4xx/5xx) |
|---|---|---|
| What happened | No reply reached your game (DNS, socket, TLS, timeout) | The server replied and rejected the request |
| Retried by EasyHTTP | Yes, when bRetryOnConnectionError is true | No, never auto-retried |
| Where you handle it | On Failure pin / retry settings | Read bSuccess and StatusCode on the response |
| Typical fix | Wait and retry the transient drop | Fix the token, URL or payload, or back off yourself |
EasyHTTP retries only the left column. Completed-but-rejected responses are handled by inspecting FEasyHTTPResponse.
Worst-case time budget examples
| Use case | Timeout (s) | RetryCount | RetryDelay (s) | Worst case (s) |
|---|---|---|---|---|
| Aggressive default | 30 | 3 | 2 | 126 |
| Tuned player-facing call | 8 | 2 | 1.5 | 27 |
| Fail-fast login gate | 5 | 1 | 1 | 11 |
Worst-case = (TimeoutSeconds x (RetryCount + 1)) + (RetryDelaySeconds x RetryCount).
FAQ
How do I handle an Unreal Engine HTTP request retry, timeout and connection error in one place?
Build an FEasyHTTPRequestOptions struct: set TimeoutSeconds for the hard time limit, set bRetryOnConnectionError to true with a RetryCount (0-10) and RetryDelaySeconds (0.1-60), then feed it to the 'Easy HTTP Request' node. Timeouts and connection-error retries are both configured per request on that single struct.
Why does EasyHTTP not retry a 500 or 404 response automatically?
Retries only fire on connection failures, never on 4xx or 5xx HTTP error responses. A server that replied with an error reached your game successfully, so retrying without changing anything would return the same error. Inspect bSuccess and StatusCode on the FEasyHTTPResponse and decide what to do, including applying your own backoff for a transient 503 if you choose.
What is the longest a single request can block my game?
Use the formula worst-case = (TimeoutSeconds x (RetryCount + 1)) + (RetryDelaySeconds x RetryCount). For example a 30 second timeout with RetryCount 3 and a 2 second delay can block for up to 126 seconds, so tune the values down for anything player-facing.
Can I test timeouts and connection errors without a real backend?
Yes. EasyHTTP ships a built-in local test server (default port 8080). Call 'Start Local Test Server', point requests at http://localhost:8080/, use 'Set Server Response' to script responses and 'Get Last Server Request' to inspect what was sent, then 'Stop All Test Servers' on End Play. The test server is HTTP-only and for development, not production.
What platforms and engine versions does EasyHTTP support?
EasyHTTP targets Unreal Engine 5.5, 5.6 and 5.7 and runs on Windows 64-bit only. HTTPS is supported through the engine's own SSL/TLS. It buffers full responses (no streaming) and does not provide WebSockets.
EasyHTTP
GET, POST, PUT and DELETE with headers, JSON parsing and async callbacks — REST APIs in a few Blueprint nodes. Talk to web services, backends and game APIs without touching C++.