TCP and HTTP with LuaSocket
LuaSocket is an extension library that adds network communication capabilities to Lua. It has a C core that handles TCP and UDP transport, plus Lua modules for HTTP, FTP, SMTP, URL parsing, and more. If you need to make HTTP requests from a script, run a TCP server, or parse URLs in your Lua program, LuaSocket is the standard tool for the job.
This guide covers TCP client and server sockets, the HTTP module, URL parsing, and the patterns you’ll reach for most often.
Installing LuaSocket
The easiest way to install LuaSocket is through LuaRocks:
luarocks install luasocket
For Lua 5.4, a known bug in receive() line-based reading was fixed in recent versions. If you hit issues, try:
luarocks install luasocket LUASOCKET_54FIX=1
You can also build from source. Grab a release from the GitHub releases page and run luarocks make on the rockspec.
Once installed, you access modules with require:
local socket = require("socket")
local http = require("socket.http")
local url = require("socket.url")
Note that LuaSocket 3.0 removed global module registrations. Older tutorials show socket.http.request() without an explicit require, but that no longer works. Always use the explicit require form.
TCP Client Sockets
A TCP socket lets you connect to a remote server and exchange data. You create one with socket.tcp(), call connect() with a host and port, then use send() and receive() to talk to the server.
local socket = require("socket")
local tcp = socket.tcp()
tcp:settimeout(5) -- 5 second timeout for all operations
-- Connect to example.com on port 80
tcp:connect("example.com", 80)
-- Send an HTTP request
tcp:send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
-- Read the response line by line
local response, err = tcp:receive("*l")
while response do
print(response)
response, err = tcp:receive("*l")
end
if err and err ~= "closed" then
print("Error:", err)
end
tcp:close()
Key methods on a TCP client socket:
connect(address, port)— connects to a remote host.addresscan be an IP or hostname.send(data)— pushes data onto the socket, returns bytes sent.receive([pattern])— reads data. Patterns:"*l"(up to newline),"*a"(all available), or an integer (exact byte count).settimeout(value)— sets I/O timeout in seconds. Pass0ornilfor infinite.close()— shuts down and closes the socket.getsockname()andgetpeername()— return local and remote address/port info.
The receive() method has a gotcha worth knowing: with "*l", it reads until a newline. If no newline arrives before the timeout, it returns nil with error "timeout" — no partial data is returned. With a byte count pattern, it tries to read exactly that many bytes and will return whatever data arrived before the timeout fired if the read couldn’t complete.
TCP Server Sockets
A TCP server socket binds to a local port and listens for incoming connections. You create it with socket.tcp(), call bind() to attach it to an address and port, then listen() to start accepting connections.
local socket = require("socket")
local server = socket.tcp()
server:setoption("reuseaddr", true) -- allow rebinding the same port quickly
server:bind("*", 8080)
server:listen(128)
print("Server listening on port 8080")
while true do
-- accept() blocks until a client connects
local client = server:accept()
client:settimeout(5)
-- Handle the client
local data, err = client:receive("*l")
if data then
print("Client sent:", data)
client:send("Echo: " .. data .. "\r\n")
else
print("Receive error:", err)
end
client:close()
end
Server socket methods:
bind(address, port)— attaches the socket to a local address and port. Use"*"for all interfaces or"127.0.0.1"for localhost only.listen(backlog)— starts listening.backlogis the connection queue size (128 is typical).accept()— blocks until a client connects, then returns a new socket representing that client connection.setoption("reuseaddr", true)— lets you restart the server quickly without waiting for the port to time out.
After accept(), the client socket has the same send(), receive(), and close() methods as a TCP client socket.
For a real server handling multiple clients, you typically run each accept() result in its own coroutine or thread. LuaSocket’s socket.select() also lets you check multiple sockets at once without blocking indefinitely on any single one.
HTTP Requests
LuaSocket’s HTTP module wraps the complexity of HTTP into two APIs: a simple string form and a table form with more control.
Simple API
For quick requests, pass a URL directly:
local http = require("socket.http")
local body, status_code, headers = http.request("https://example.com/")
print(status_code) -- e.g. 200
print(#body, "bytes received")
The simple form returns three values: the response body, the status code, and a table of response headers.
Table API
When you need custom headers, a specific HTTP method, or a request body, use the table form:
local http = require("socket.http")
local ltn12 = require("ltn12")
local response_body = {}
local res, status_code, headers = http.request {
url = "https://api.github.com/repos/lua/lua/stats/contributors",
method = "GET",
headers = {
["Accept"] = "application/json",
["User-Agent"] = "LuaSocketClient/1.0"
},
sink = ltn12.sink.table(response_body)
}
if status_code == 200 then
local data = table.concat(response_body)
print("Received:", #data, "bytes")
end
Key options for the table form:
url(string) — the target URL, required.method(string) — HTTP verb, defaults to"GET".headers(table) — extra HTTP headers.sink(LTN12 sink) — where to write the response body.ltn12.sink.table()collects it into a table.source(LTN12 source) — provides the request body.redirect(boolean) — whether to follow 301/302 redirects, defaults totrue.maxredirects(number) — maximum redirects to follow.
For a POST request with a JSON body:
local http = require("socket.http")
local ltn12 = require("ltn12")
local json = require("json") -- or dkjson
local request_body = json.encode({ name = "test", value = 42 })
local response_body = {}
local res, status_code, headers = http.request {
url = "https://httpbin.org/post",
method = "POST",
headers = {
["Content-Type"] = "application/json",
["Content-Length"] = tostring(#request_body)
},
source = ltn12.source.string(request_body),
sink = ltn12.sink.table(response_body)
}
print("Status:", status_code)
One important pitfall: http.request() returns nil as the body when the status code indicates an error. Always check all three return values:
-- WRONG:
local body = http.request("http://example.com/")
-- body may be nil even if the request succeeded with no body
-- RIGHT:
local body, status_code, headers = http.request("http://example.com/")
if not body then
print("Request failed, status:", status_code)
end
URL Parsing
The URL module parses URLs into components and builds URLs from tables. This is useful when you need to extract the host, path, or query string from a URL.
local url = require("socket.url")
local parsed = url.parse("https://api.example.com:8080/v1/users?id=42#profile")
print(parsed.host) --> api.example.com
print(parsed.port) --> 8080
print(parsed.path) --> /v1/users
print(parsed.query) --> id=42
print(parsed.fragment) --> profile
print(parsed.scheme) --> https
You can also build a URL from a table:
local built = url.build {
scheme = "https",
host = "api.example.com",
port = 443,
path = "/v1/search",
query = "q=luasocket"
}
print(built) --> https://api.example.com/v1/search?q=luasocket
Other useful URL module functions:
url.absolute(base, relative)— resolves a relative URL against a base URL.url.parsePath(path)andurl.buildPath(parsed)— work with just the path component.
Common Patterns
Setting Timeouts
Timeouts apply to all subsequent operations on a socket. Set them early:
local tcp = socket.tcp()
tcp:settimeout(10) -- 10 seconds for connect, send, receive
-- You can also set different timeouts:
tcp:settimeout(5, "b") -- "b" for blocking mode timeout (same as above)
tcp:settimeout(2, "r") -- "r" for receive-only
tcp:settimeout(3, "w") -- "w" for write-only
Non-blocking with socket.select()
socket.select() checks whether sockets are ready for reading or writing without blocking indefinitely. Pass two arrays of sockets and a timeout in seconds:
local socket = require("socket")
local tcp = socket.tcp()
tcp:connect("example.com", 80)
tcp:settimeout(0) -- non-blocking mode
-- Send a request
tcp:send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
-- Poll until data is available (max 5 seconds)
local read_sockets, write_sockets, err = socket.select({tcp}, nil, 5)
if read_sockets and read_sockets[1] then
local response, err = tcp:receive("*a")
if response then
print("Got", #response, "bytes")
end
end
socket.select(readable, writable, timeout) returns sockets that are ready from each array. Pass nil for whichever direction you don’t care about.
UDP Sockets
UDP is connectionless and has no handshake phase. Instead of connect(), UDP uses setpeername() to set a default recipient — this doesn’t establish a session, it just tells the socket where to send data by default:
local udp = socket.udp()
udp:setpeername("example.com", 12345) -- sets default destination
udp:send("hello") -- sends to the configured peer
-- Receive returns data and sender address
local data, addr = udp:receive()
print("Got:", data, "from", addr)
LuaSocket vs Alternatives
LuaSocket works in any Lua environment, but OpenResty environments typically use lua-resty-http instead. That library leverages nginx’s event loop for true non-blocking I/O and built-in connection pooling. For a standalone Lua script or a CLI tool, LuaSocket is the right choice. For a production web service running behind nginx, lua-resty-http performs better.
See Also
- The /tutorials/modules-and-require/ guide covers how Lua’s module system works, which is what powers
require("socket"). - The /tutorials/coroutine-basics/ tutorial explains coroutines, which pair well with
socket.select()for handling multiple connections in one thread. - The /guides/lua-serialization/ guide covers encoding and decoding data for transmission over sockets.
Conclusion
LuaSocket gives Lua programs straightforward access to TCP and HTTP networking. The socket.tcp() API covers raw TCP clients and servers, socket.http wraps HTTP at two levels of abstraction, and socket.url handles URL parsing. All operations are blocking by default, so combine sockets with coroutines or socket.select() when you need concurrency. For most scripts and tools, LuaSocket is all you need.