Problem
core/server.lua event loop calls luasocket.select which wraps the OS select() syscall via the bundled luasocket/src/select.c. On Linux, glibc's fd_set is hardcoded to 1024 bits in <sys/select.h> (__FD_SETSIZE). The hub silently caps at ~1000 concurrent connected sockets — connection #1024+ fails with EINVAL and the operator sees timeouts.
ulimit -n N does not help: it raises the per-process FD limit but the fd_set bitmask size is fixed at compile-time inside glibc.
Originally reported upstream as luadch/luadch#1 (2015, still open). Upstream maintainer's plan in 2020: "Eventually I will replace select by poll on unix". Never happened.
Why this isn't a one-line CMake bump
-DFD_SETSIZE=8192 only works on Windows. On Linux, glibc's fd_set typedef has a fixed buffer of 128 longs (1024 bits); calling FD_SET(2048, &set) writes past the struct. Buffer overflow, undefined behavior. The fd-bound guard in luasocket/src/select.c:140 is exactly to prevent that overflow.
Only path on Linux: replace select() with poll() (no bitmask, takes an struct pollfd[] array).
Code surface
| File |
Function |
Role |
LoC change |
luasocket/src/select.c |
global_select() |
Lua-facing luasocket.select — main hub event loop |
~80-100 |
luasocket/src/usocket.c |
socket_waitfd() |
Per-socket wait inside connect/send/receive. if (*ps >= FD_SETSIZE) return EINVAL → every operation on fd ≥ 1024 fails, not just the event loop |
~25-30 |
luasocket/src/wsocket.c |
socket_waitfd() + socket_select() |
Windows variants, WSAPoll instead of poll |
~50-60 |
luasocket/src/socket.h |
prototypes |
+5 |
|
luasocket/src/luasocket.c |
register luasocket.poll |
+5 |
|
core/server.lua |
luasocket.select → luasocket.poll |
+3 |
|
luasocket/CMakeLists.txt |
-DLUASOCKET_USE_POLL build flag |
+10 |
|
tests/smoke/ |
new smoke opening >1024 sockets |
+60-80 |
|
Total: ~200 LoC C + ~10 Lua + ~80 LoC test = ~290 LoC.
Tiers
- Linux poll() backend, opt-in via CMake (
-DLUASOCKET_USE_POLL=ON, default OFF). 2-3 days.
- Windows WSAPoll backend, same flag. WSAPoll has a historic non-blocking
connect() reporting bug — workaround via getsockopt(SO_ERROR) after POLLWRNORM. 2-3 days.
- Default ON + BUILDING.md update. 1 day.
Realistic total: 6-8 focused workdays, plus 1-2 days buffer for WSAPoll quirks.
Behaviour-difference risks
poll() distinguishes POLLHUP / POLLERR / POLLIN; select() doesn't. Current code does not inspect why a socket is readable — just calls receive() and lets that report the error. Map POLLHUP|POLLERR|POLLNVAL → readable in the wrapper to preserve behaviour 1:1.
- Smoke test needs a synthetic >1024-socket load (pairs via
socketpair() or many localhost connects). Today's smoke does not exercise this regime.
Out of scope
epoll() on Linux for O(1) event delivery (vs poll's O(N) array scan). Worth doing if hub regularly serves 5000+ concurrent users; not before.
- libevent / libev / cqueues replacement of luasocket. Too invasive, this issue is the minimal fix.
- Replacing luadch with
uhub for mass-hub use cases (upstream blasti's 2015 suggestion). Out of project scope.
Upstream maintenance
luasocket releases are slow (3.0 in 2013, 3.1 in 2022). Re-porting the poll diff at each upstream bump: ~1-2h.
When this is worth doing
Today, the hubs we know of (Sopor / similar) run <500 concurrent users — the limit is theoretical. This issue is for when:
- a luadch operator wants to host >800 concurrent users, OR
- we want to advertise luadch as suitable for public mass-hubs.
Until then, later label.
Problem
core/server.luaevent loop callsluasocket.selectwhich wraps the OSselect()syscall via the bundledluasocket/src/select.c. On Linux, glibc'sfd_setis hardcoded to 1024 bits in<sys/select.h>(__FD_SETSIZE). The hub silently caps at ~1000 concurrent connected sockets — connection #1024+ fails withEINVALand the operator sees timeouts.ulimit -n Ndoes not help: it raises the per-process FD limit but thefd_setbitmask size is fixed at compile-time inside glibc.Originally reported upstream as luadch/luadch#1 (2015, still open). Upstream maintainer's plan in 2020: "Eventually I will replace select by poll on unix". Never happened.
Why this isn't a one-line CMake bump
-DFD_SETSIZE=8192only works on Windows. On Linux, glibc'sfd_settypedef has a fixed buffer of 128 longs (1024 bits); callingFD_SET(2048, &set)writes past the struct. Buffer overflow, undefined behavior. The fd-bound guard inluasocket/src/select.c:140is exactly to prevent that overflow.Only path on Linux: replace
select()withpoll()(no bitmask, takes anstruct pollfd[]array).Code surface
luasocket/src/select.cglobal_select()luasocket.select— main hub event loopluasocket/src/usocket.csocket_waitfd()if (*ps >= FD_SETSIZE) return EINVAL→ every operation on fd ≥ 1024 fails, not just the event loopluasocket/src/wsocket.csocket_waitfd()+socket_select()luasocket/src/socket.hluasocket/src/luasocket.cluasocket.pollcore/server.lualuasocket.select→luasocket.pollluasocket/CMakeLists.txt-DLUASOCKET_USE_POLLbuild flagtests/smoke/Total: ~200 LoC C + ~10 Lua + ~80 LoC test = ~290 LoC.
Tiers
-DLUASOCKET_USE_POLL=ON, default OFF). 2-3 days.connect()reporting bug — workaround viagetsockopt(SO_ERROR)afterPOLLWRNORM. 2-3 days.Realistic total: 6-8 focused workdays, plus 1-2 days buffer for WSAPoll quirks.
Behaviour-difference risks
poll()distinguishesPOLLHUP/POLLERR/POLLIN;select()doesn't. Current code does not inspect why a socket is readable — just callsreceive()and lets that report the error. MapPOLLHUP|POLLERR|POLLNVAL → readablein the wrapper to preserve behaviour 1:1.socketpair()or many localhost connects). Today's smoke does not exercise this regime.Out of scope
epoll()on Linux for O(1) event delivery (vs poll's O(N) array scan). Worth doing if hub regularly serves 5000+ concurrent users; not before.uhubfor mass-hub use cases (upstream blasti's 2015 suggestion). Out of project scope.Upstream maintenance
luasocket releases are slow (3.0 in 2013, 3.1 in 2022). Re-porting the poll diff at each upstream bump: ~1-2h.
When this is worth doing
Today, the hubs we know of (Sopor / similar) run <500 concurrent users — the limit is theoretical. This issue is for when:
Until then,
laterlabel.