SysLibSockets has no connection limit of its own, and three TCP clients is a trivial load for any target. A client that runs for hours and then stops is leaking socket handles or blocking its task past the watchdog. Fix it with one non-blocking state machine per connection and a single CLOSE state that releases every handle.
Three TCP connections is a small load. SysLibSockets carries no connection limit of its own: the ceiling belongs to the TCP/IP stack on your target, and any industrial stack holds dozens of sockets before it refuses more. A client that runs for hours and then stops is telling you something else. Two faults produce exactly that pattern: socket handles created on every retry and never closed, and a call that blocks until the task watchdog halts the program. This page maps each symptom to its fault, gives the state-machine structure that removes both, and lists the numbers to watch while you prove the fix.
Is there a maximum number of TCP connections in SysLibSockets?
No. SysLibSockets has no connection limit of its own, and three simultaneous TCP clients is a trivial load for any CODESYS target: the ceiling is set by the target's TCP/IP stack, and a stack that could not hold three sockets would fail in the first minute, not after hours.
The library passes sockets to the stack underneath it and adds no count of its own. If you want the ceiling as a number for your controller, measure it: create sockets in a test program until the create call returns the invalid-handle value, then read the library's last-error function at that failure. That count is your limit, and on an industrial target it is dozens or more. Treat the connection-count question as answered and look at how the client manages handles and blocking.
Why does the client work for hours and then stop?
Two mechanisms produce the hours-then-stop pattern: the code creates a socket handle on every retry and never closes the old one until the stack's socket table fills, or a call blocks and the task runs past its watchdog.
The leak arrives on a schedule. Every reconnect that creates a socket without closing the previous handle consumes one entry in the stack's socket table. The table is finite and the leak is steady, so the stop lands a fixed number of reconnects after startup, which looks like a mystery when you only see the end of it.
The block arrives in one scan. A blocking connect to an unreachable address waits on the stack's own timeout, commonly tens of seconds. A task watchdog set to 200 ms halts the task long before the call returns, and the runtime writes a task exception with the cycle time that broke the limit. No socket error appears, because no socket call ever finished. If the diagnostic log shows the task stopped on cycle time, work the sporadic runtime fault troubleshooting path and leave the network alone; the wiring is fine.
Which symptom points to which fault?
The fastest split is whether a socket error appears at the stop: a halted task with no socket error points at the watchdog, a create call returning the invalid-handle value points at a full socket table, and a receive error after a long quiet period points at a dead peer.
| Symptom at the stop | What it points to | Check |
|---|---|---|
| Task halted, no socket error in the log | Blocking call tripped the task watchdog | Task monitor: maximum cycle time against watchdog time |
| Create returns invalid handle, last error says no sockets left | Handle leak filling the stack's socket table | Counters: sockets created minus sockets closed, trending up |
| Fails the same number of hours after every restart | Leak tied to a fixed reconnect interval, such as a peer dropping idle sessions | Count reconnects since startup and divide by run time |
| Stops only when the network changes: cable pull, peer reboot, switch replacement | Blocking connect, or a half-open connection with no timeout of its own | Pull the peer cable on purpose and watch the cycle time |
| Connection reset error after the peer restarted | Code kept using a handle the peer had already closed | Close and recreate the handle after any receive error |
Read the table top down. The first two rows cover most cases on this library, and both are removed by the same restructure.
Should the three connections use blocking or non-blocking sockets?
Use non-blocking sockets for all three, driven from one cyclic task: every call returns immediately, one dead peer delays nothing, and the watchdog sees normal cycle times.
A blocking call that waits holds the whole cycle behind it, the same failure class as an HMI script blocking on a long-running loop. Splitting the three connections into three tasks isolates a stall to one task, but it multiplies your watchdog margins by three and leaves the leak risk untouched. Non-blocking mode removes the wait at the source: connect reports in progress, send and receive report would-block, and your code decides what to do next scan. That is the reason the state machine below exists.
How do I put a socket into non-blocking mode?
Switch the socket immediately after creation with the library's ioctl call and the FIONBIO request, passing a DINT set to 1; from then on connect, send and receive return a would-block indication instead of waiting.
Declare diNoBlock : DINT := 1; and call SysSockIoctl(diSocket, SOCKET_FIONBIO, ADR(diNoBlock)) once, right after the create succeeds. Check the return value the way you check every other call: a failed ioctl leaves the socket blocking, and every later call inherits the wait. From this point, treat the would-block indication as try-again-next-cycle and every other non-success result as a fault into CLOSE.
How do I build one state machine per connection?
Give each connection its own function block with six states, IDLE, CREATE, CONNECT, RUN, CLOSE and WAIT, its own socket handle and its own receive buffer, and route every fault into CLOSE so the handle is released in exactly one place.
- IDLE: the handle holds the invalid value and the timers are clear.
- CREATE: create the socket once per pass, set it non-blocking, and on an invalid handle go to WAIT.
- CONNECT: issue the connect and poll for completion under an application timer of 5 s. Would-block means keep polling; a timeout or any hard error goes to CLOSE with the last-error value stored.
- RUN: send and receive. Resend the remainder when a send returns fewer bytes than requested. Append received bytes to the buffer and parse by length field or delimiter, because one receive call is never one message on a TCP stream. Close after 30 s with no traffic in either direction; a half-open connection raises no error until data moves.
- CLOSE: if the handle is valid, close it and log a failing close, then write the invalid value back. This is the only statement in the block that releases a handle, and every fault path lands here.
- WAIT: hold 5 s, then return to CREATE. A retry every scan floods the peer and eats handles.
Instantiate the block three times in one task. Which task should own communication code is an execution-order question, and PLC execution order across task layers shows how to trace where the calls land each cycle. The framing you parse in RUN is an interface design question, covered from the peer side in PLC parameter lookup over Ethernet.
Recommendation: rebuild all three connections as non-blocking state machines in one cyclic task, with CLOSE as the only statement that releases a handle, before you touch any network component. Keep blocking calls only if the target documentation states the runtime offers no non-blocking mode, and then give each connection its own task with its own watchdog margin.
What does a leaked handle look like in a worked example?
Assume the target stack allocates 128 sockets and the peer closes idle sessions every 5 minutes: one leaked handle per reconnect exhausts the table in 128 reconnects, about 10.7 hours, which is exactly the runs-fine-then-stops pattern.
The arithmetic: 128 available handles at 1 handle lost per 5 minutes is 128 x 5 = 640 minutes, or 10 hours 40 minutes. At that moment the create call returns the invalid-handle value, the retry logic treats it as a transient fault, and the client spins until someone restarts it. A restart clears the table, the clock starts again, and the failure returns the next day at the same hour. That repetition is your signature: the stop is tied to reconnect count.
Slide one number and the failure moves with it. At one reconnect per hour, the same 128 handles last 128 hours, just over five days. Divide your observed time-to-failure by your reconnect interval and you have the socket count your stack gives you, to within the jitter of the interval.
With the state machine in place, the same peer dropping sessions every 5 minutes closes one handle for every handle it creates, the created-minus-closed counter returns to 0 after every drop, and the run continues past every previous failure point.
How do I confirm the stall is gone?
Give each connection three diagnostics, sockets created, sockets closed and last error, and require created minus closed to sit at 0 when idle and 1 when connected across a 48 hour run with faults injected on purpose.
- Watch the task monitor with one peer unplugged and one address made unroutable: with a 200 ms watchdog, keep the maximum cycle time under 100 ms.
- Pull each peer's cable and power-cycle each peer several times. Every connection must pass through CLOSE and WAIT and reconnect with no operator action.
- Mirror the switch port and capture packets: one FIN or RST per closed session, retries spaced at the 5 s WAIT delay, and no growing pile of unanswered SYNs.
- Run 48 hours unattended, longer than the old time-to-failure ever was, with the counters visible. Flat counters and a flat cycle time close the case.
Do the cable pulls on a bench system or outside production: on a running line, a dropped session can stop the process the peer serves. Once the three connections hold their data, where live factory data should surface, screen or dashboard, is the next decision worth making before anyone builds a second path.
Questions
Does socket keepalive replace the idle timeout?
No. Keepalive is off unless the target exposes the socket option and you set it, and its default interval on industrial stacks is often an hour or more, far too slow to matter. An application timer that closes the connection after 30 seconds of silence detects a dead peer within half a minute. Use keepalive only as a complement, set to the same order as your timer, if the target lets you change it.
Do I still need a heartbeat if the peer sends data several times a minute?
No. An idle timeout of about 30 seconds catches a silent peer whenever the normal gap between messages is a few seconds. Add an application-level heartbeat only when quiet periods of minutes are normal, for example a controller that reports on event. A half-open connection raises no error until data moves, so some timer has to bound the silence either way.
Where do I look when the task stops and no socket error is logged?
In the runtime's task diagnostic or exception log. A watchdog trip is recorded there with the task name and the cycle time that exceeded the limit, while socket faults surface through the library's last-error value instead. If the log shows the task halted on cycle time, the blocking-call diagnosis is confirmed and no network component is at fault.
How many connections can one cyclic task drive this way?
Tens. Each state machine costs a few comparisons per cycle, so the practical limits are the stack's socket table and the data rate you parse, and both sit far above three. If you plan to pass ten or more, keep one receive buffer per connection, size each buffer to your longest frame, and watch the created-minus-closed counters across all instances, because a leak in one instance starves the others.
Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.