TCP and HTTP with LuaSocket

· 8 min read · Updated April 2, 2026 · intermediate
luasocket tcp http networking sockets

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. 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" — 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.

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. 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) and url.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.