luaguides

TCP and HTTP Networking in Lua with LuaSocket

LuaSocket is the standard extension library for TCP and HTTP networking in Lua. It provides a C core for TCP and UDP transport paired with Lua modules for HTTP, FTP, SMTP, and URL parsing. If you need to make HTTP requests from a script, run a TCP server, or parse URLs in a Lua program, LuaSocket is the 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 the build with the compatibility flag:

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. The source approach is useful when you need a specific commit or want to inspect the C code before linking it into your Lua runtime.

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. address can 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. Pass 0 or nil for infinite.
  • close(); shuts down and closes the socket.
  • getsockname() and getpeername(); 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", meaning 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. backlog is 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. This is enough for most one-off fetches, but you lose control over headers and the HTTP method.

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 to true.
  • 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, and it also returns nil for successful responses that genuinely have no body. Always check all three return values to distinguish between these two cases:

-- 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. Every HTTP response carries metadata in its headers and the URL itself often encodes structural information such as API versions or resource identifiers. Parsing URLs lets you work with that structure programmatically rather than through manual string manipulation.

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

Parsing a URL is only half the story. When your application needs to construct request URLs dynamically, building them from components is far safer than concatenating strings. A single misplaced slash can redirect a request to the wrong endpoint:

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) and url.buildPath(parsed); work with just the path component.

Common Patterns

Setting Timeouts

Timeouts apply to all subsequent operations on a socket, and every operation inherits the timeout that was set last. Setting them early avoids a situation where connect() blocks indefinitely because the remote host never responds:

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. This is LuaSocket’s answer to the problem of handling multiple connections in a single thread without threads or coroutines:

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. Note that socket.select() works only with LuaSocket socket objects, not with raw file descriptors or sockets from other libraries.

One subtlety with non-blocking mode is that send() may not transmit all the data you gave it in one call. The return value tells you how many bytes actually went out, and you are responsible for sending the rest. A production client typically wraps send() in a loop that retries until every byte has been transmitted.

UDP Sockets

UDP is connectionless and has no handshake phase. Instead of connect(), UDP uses setpeername() to set a default recipient; this does not 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.

Conclusion

LuaSocket gives Lua programs straightforward access to TCP and HTTP networking. The socket.tcp() API covers raw TCP clients and servers with full control over timeouts and non-blocking I/O, 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 for networking in Lua.

See Also

  • Modules and require covers how Lua’s module system works, which is what powers require("socket").
  • Coroutine basics explains coroutines, which pair well with socket.select() for handling multiple connections in one thread.
  • Serialization in Lua covers encoding and decoding data for transmission over sockets.