Quick answer: WebSocketPeer does nothing unless you call poll() every frame, and it is reference-counted: if it lives in a local variable, it is freed (and the connection closed) when the function returns. Store it in a member variable, poll it in _process(), and read get_close_code() / get_close_reason() when it closes.
Godot 4's WebSocketPeer is a low-level object. It does not run itself and it does not keep itself alive, and those two facts explain most connections that "close immediately".
A minimal client that works
extends Node
var socket := WebSocketPeer.new() # member variable, not local
func _ready():
var err := socket.connect_to_url("wss://example.com/ws")
if err != OK:
push_error("connect_to_url failed: %s" % err)
func _process(_delta):
socket.poll() # required every frame
match socket.get_ready_state():
WebSocketPeer.STATE_OPEN:
while socket.get_available_packet_count() > 0:
print(socket.get_packet().get_string_from_utf8())
WebSocketPeer.STATE_CLOSED:
print("closed: %d %s" % [socket.get_close_code(), socket.get_close_reason()])
set_process(false)
Cause 1: no poll()
Without poll(), the handshake never progresses, packets are never read, and the server eventually times the client out. Poll in _process() (or _physics_process()), also while connecting.
Cause 2: the peer is a local variable
func connect_server():
var ws := WebSocketPeer.new() # freed when this function returns
ws.connect_to_url(url)
WebSocketPeer is RefCounted. When the last reference goes away, it is freed and the socket closes. Keep it in a member variable or an autoload.
Cause 3: sending before the connection is open
send_text() before STATE_OPEN fails. Queue messages and send them when the state first becomes open.
Cause 4: TLS problems with wss://
wss:// needs a certificate the client trusts. Self-signed certificates on a development server are rejected; for local testing pass TLSOptions.client_unsafe() to connect_to_url() (never in release builds), or use ws:// on localhost.
Cause 5: web exports and mixed content
A game served over https:// can only open wss:// sockets; the browser blocks ws:// from a secure page before Godot sees anything. Check the browser console. CORS does not apply to WebSockets, but the server may still reject unknown Origin headers.
Cause 6: the server closes it
Read the close code. 1000 is a normal close, 1006 means the connection dropped without a close frame (often a proxy or timeout), 1002 or 1003 point at a protocol mismatch. If the server requires a subprotocol, set socket.supported_protocols before connecting. Idle connections behind proxies are commonly cut after 60 seconds; send periodic pings, or set heartbeat_interval where your Godot version supports it.