10 Common Mistakes Using sgcWebSockets (and How to Fix Them)

· Components

After many years answering support tickets, the same handful of issues account for the vast majority of "my WebSocket connection is acting weird" reports. None are bugs in the library — all are configuration mistakes that take five minutes to fix once you know they exist. This post lists the ten most common, with the symptom, the cause, and the one-liner that resolves each one.

If you only read one mistake from this list, make it #2. Blocking the OnMessage event is responsible for more "the library is slow" tickets than every other category combined, and it is invisible until you scale beyond a few hundred concurrent clients. Save yourself the late-night production debugging session and fix it before you ship.

Mistake 1: WatchDog Reconnect Disabled

Symptom: the client connects on startup, runs fine for hours, then silently stops receiving messages after a Wi-Fi blip, a VPN reconnect, or a server restart.

Cause: the WatchDog property is off by default. The library does not assume you want automatic reconnects — some apps want fail-fast behaviour. Most don't.

oClient.WatchDog.Enabled  := True;
oClient.WatchDog.Interval := 10;   // try every 10 seconds
oClient.WatchDog.Attempts := 0;    // 0 = unlimited

Fire-and-forget. Combine it with OnDisconnect logging so you can see in the field how often reconnects fire. For mobile or laptop deployments where the user roams between networks, also wire up the OS network-change notification (SystemEvents.NetworkAvailabilityChanged on .NET, the WMI route on Windows-Delphi) to trigger an immediate reconnect rather than waiting for the next interval tick.

Mistake 2: Blocking the OnMessage Event

Symptom: throughput collapses under load. The server feels "stuck". CPU is low, but messages back up.

Cause: OnMessage runs on the I/O worker thread. If you call a slow database, an external HTTP API, or a long-running parser inside it, you starve every other connection sharing that worker.

// BAD: blocks the worker
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
  oDb.Query('INSERT INTO events VALUES(?)', [Text]);     // 50 ms
  oHttp.Get('https://billing/track?event=' + Text);      // 200 ms
end;

// GOOD: hand off to a worker queue
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
  oWorkQueue.Push(TWorkItem.Create(Connection.Guid, Text));
end;

Mistake 3: Ignoring HeartBeat

Symptom: ghost connections accumulate. Connections.Count keeps growing even though you know clients have died.

Cause: without application-layer heartbeats, the OS TCP keepalive timer fires after ~2 hours. By then you have tens of thousands of zombie sockets eating memory.

oServer.HeartBeat.Enabled  := True;
oServer.HeartBeat.Interval := 30;
oServer.HeartBeat.Timeout  := 90;

Mistake 4: Mismatched TLS Versions

Symptom: client gets "handshake failed" or "SSL_ERROR_SYSCALL". Connection works on a dev box but fails in production behind a corporate proxy.

Cause: the client SSL options default to TLS 1.0–1.2 for backward compatibility; modern servers require TLS 1.2/1.3 only. Or the OpenSSL DLLs are old.

oClient.TLSOptions.Version := tlsTLSv1_2;     // or tlsTLSv1_3
oClient.TLSOptions.OpenSSL_Options.LibPath := ExtractFilePath(ParamStr(0));
// Bundle the latest libcrypto-3.dll / libssl-3.dll with your installer.

Mistake 5: Not Handling Fragmented Messages

Symptom: the server receives the first part of a large JSON document, your parser fails, and the connection is closed.

Cause: by default, sgcWebSockets reassembles fragments for you and only fires OnMessage with the complete payload. But if you have set ReadOptions.FragmentMode := frgPartial for memory reasons, you must reassemble.

// Default (recommended for most apps)
oServer.ReadOptions.FragmentMode := frgComplete;

// If you opted into partial delivery, reassemble manually
procedure TForm1.OnFragment(Connection: TsgcWSConnection;
  const aPartial: TBytes; aIsFinal: Boolean);
begin
  Buffers[Connection.Guid].Append(aPartial);
  if aIsFinal then
    Process(Buffers[Connection.Guid].ToBytes);
end;

Mistake 6: Using Synchronous API in the GUI Thread

Symptom: the UI freezes for several seconds when connecting, or when calling WriteData on a slow link.

Cause: blocking calls on the main thread. Always use the async pattern in VCL/FMX apps, or call from a worker thread.

// BAD: blocks the UI
oClient.Active := True;

// GOOD: connect asynchronously
TTask.Run(procedure
  begin
    oClient.Active := True;
  end);

// Or use the event-driven API
oClient.OnConnect := DoConnected;
oClient.Connect;  // returns immediately

Mistake 7: Forgetting to Select a Sub-Protocol

Symptom: connection succeeds but the peer rejects every frame, or responds with a different format than expected.

Cause: many WebSocket servers (MQTT-over-WS, STOMP, GraphQL-WS, Phoenix) require a specific sub-protocol in the handshake. Without it, the server defaults to a different protocol or drops you.

// MQTT over WebSocket
oClient.Specifications.Hixie76    := False;
oClient.Specifications.RFC6455    := True;
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: mqttv3.1');

// GraphQL over WS
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: graphql-ws');

Mistake 8: Buffer Sizes Too Small (or Too Large)

Symptom: high CPU on a server pushing many small messages, or out-of-memory on a server pushing big ones.

Cause: default send/receive buffers are sized for general use. Tune them to your traffic shape.

// Small messages (chat, telemetry): smaller buffers reduce per-conn memory
oServer.IOHandler.RecvBufferSize := 4096;
oServer.IOHandler.SendBufferSize := 4096;

// Big messages (file transfer, media): larger buffers reduce syscalls
oServer.IOHandler.RecvBufferSize := 65536;
oServer.IOHandler.SendBufferSize := 65536;

Mistake 9: No Origin Check on Public Servers

Symptom: security audit flags "any website can connect to your WebSocket via a user's browser".

Cause: the WebSocket protocol does not enforce same-origin. Your server must validate the Origin header.

procedure TForm1.ServerBeforeConnect(Connection: TsgcWSConnection;
  var Continue: Boolean);
var
  vOrigin: string;
begin
  vOrigin := Connection.Headers.Values['Origin'];
  Continue := (vOrigin = 'https://app.example.com')
           or (vOrigin = 'https://admin.example.com');
end;

Mistake 10: Logging Sensitive Data in OnMessage

Symptom: auditor finds API keys, JWTs, or PII in log files. Compliance issue.

Cause: the easy LogMemo.Lines.Add(Text) in OnMessage writes every payload to disk forever.

procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
  // Hash / redact before logging
  LogMemo.Lines.Add(Format('[%s] %d bytes (sha=%s)',
    [Connection.Guid, Length(Text), ShortHash(Text)]));
  Process(Text);
end;

Bonus: Not Reading the History.txt

Every release ships with a history.txt listing every change, fix, and breaking note since 2013. Five minutes spent skimming it after each upgrade saves hours of "why did this stop working" debugging later.

Bonus 2: Mixing Component Versions Across Projects

Delphi developers sometimes copy a single .pas from a newer sgcWebSockets release into an older project, "just this one file". This works until it doesn't — the file depends on types that changed two releases ago, and the linker mysteriously fails or, worse, links but crashes at runtime. Always upgrade the whole library together. The 30 seconds saved by copying one file is not worth the four hours of debugging when something downstream breaks.

Bonus 3: Treating WebSocket as Fire-and-Forget

WebSocket is not a message queue. It is a bidirectional byte stream over TCP. If the network drops mid-message, the frame is lost and never redelivered automatically. For business-critical messages you must add your own acknowledgement protocol on top — usually a per-message UUID, an explicit ACK from the receiver, and a resend on the sender after a timeout. Skipping this layer is fine for "user is typing" notifications and fatal for "user paid for cart".

Bonus 4: Letting Memos Bleed Memory

Symptom: the diagnostic memo on your debug form is the first thing to OOM your client after a few hours of traffic. You blame sgcWebSockets; sgcWebSockets is innocent.

Cause: a TMemo retains every line ever added. At 100 lines per second that is 360,000 lines per hour. Each line allocates a string. The VCL renders thousands of invisible lines on every WM_PAINT. Your process grinds to a halt while the library is doing nothing wrong.

// Cap the diagnostic memo
procedure TForm1.LogLine(const aText: string);
const
  cMaxLines = 500;
begin
  TThread.Queue(nil, procedure
    begin
      while Memo1.Lines.Count > cMaxLines do
        Memo1.Lines.Delete(0);
      Memo1.Lines.Add(aText);
    end);
end;

Even better: log to a rolling file via LoggerPro and only mirror the last 200 lines into the memo for visual debugging. Production code should never write to a UI control from a network thread.

Bonus 5: Not Closing the Server Before Application.Terminate

Symptom: on app shutdown the process hangs for 30 seconds, or the OS reports unhandled exceptions in client logs because connections were torn down ungracefully.

Cause: the server destructor sends a close frame to each connection and waits for the OS to release the listening port. If you call Application.Terminate before oServer.Active := False, the connections die mid-handshake and the OS port stays in TIME_WAIT, blocking a quick restart.

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  if oServer.Active then
  begin
    oServer.Broadcast('{"event":"shutdown","reconnect_after":5}');
    Sleep(200);                  // let frames flush
    oServer.Active := False;     // graceful close
  end;
end;

For console servers, hook SetConsoleCtrlHandler on Windows or SIGTERM on Linux and run the same shutdown sequence. Pair this with a HUP/restart loop in your service manager and you have zero-dropped-connection deploys.

The Pattern Behind the Patterns

Most of these mistakes have a common root: assuming the network is reliable. It is not. Half-open TCP connections happen. Mobile networks drop. Corporate proxies break TLS. Wi-Fi roams. Servers restart. Cloud load balancers terminate idle connections after 60 seconds. A WebSocket app that does not survive these conditions is not finished — it is a happy-path demo. The good news: the library exposes a control for every one of these scenarios. The bad news: most of them are off by default for backward compatibility. Two hours spent reading the WatchDog, HeartBeat, Reconnect, and TLS option pages of the documentation is the cheapest insurance you will ever buy.

Second-order pattern: respect the I/O thread. Anything that takes more than a millisecond — database query, file I/O, external HTTP call, regex on a long string, JSON parse on a 100 KB payload — belongs on a worker thread, not in OnMessage. Make this rule absolute. Junior developers will violate it three months from now; a code-review checklist that includes "no blocking in OnMessage" catches it before it ships.

Where to Go Next

If you are tuning a high-traffic server, read sgcWebSockets Performance Tuning next. New to the library? Start at the Getting Started hub and connect your first WebSocket in five minutes.