Serial Communication App Pitfalls - Through Reconnection and Log Design
· Updated: · Go Komura · Serial Communication, RS-232, C#, .NET, Windows Development, Device Integration
Device integration, measuring instruments, PLCs, barcode readers, USB-to-serial adapters. Serial communication looks like old technology, yet it is still entirely commonplace in real Windows application work.
The slightly dangerous part is that serial communication can be started with nothing more than a single COM port and a single Read / Write. The connectivity check passes immediately, but once in production, symptoms like the following tend to appear.
- Commands and responses occasionally get out of sync
- It freezes exactly once a day
- It fails to recover only after a USB unplug/replug
- The UI sometimes stalls
- The logs contain nothing but “Timeout”
What is genuinely hard in serial communication apps is not the send/receive API itself, but boundaries, timeouts, state transitions, reconnection, and observability.
1. The Conclusions First
Summarized up front, in practice-oriented terms.
- Serial communication is an ordered byte stream; message boundaries do not appear on their own
- Calling
Read(100)does not guarantee exactly 100 bytes back - .NET’s
DataReceivedis not guaranteed to fire per received byte, and moreover it is not on the UI thread ReadLine()/WriteLine()behave nicely only when the peer truly speaks a line-based text protocol- One timeout is not enough. Separating the meanings —
open,inter-byte,response,reconnect— gives more stability - Rather than allowing
Writefrom anywhere, leaning toward a single writer is harder to break - With USB-serial, it is more peaceful to assume from the start: unplug/replug, re-enumeration, COM number changes, and reconnection failures
In short, the hard part of a serial communication app is not “can you open the port,” but how you turn the byte stream into meaningful messages, and how you manage the time and state around it.
2. Serial Communication Is an “Ordered Byte Stream,” Not “Messages”
From the application’s point of view, serial communication looks like “send one command, receive one response.” But at the layer below, what actually flows is just an ordered sequence of bytes.
What you sent with one Write may appear to the other side as:
- Arriving in one
Read - Arriving split into two
- Arriving concatenated with other data
Drop this premise and the app starts assuming “this Read must be this response.” That assumption is often the first land mine in serial communication apps.
| Common assumption | Reality |
|---|---|
Read(16) returns exactly 16 bytes |
Depending on arrival and timeouts, you may only get part of it |
DataReceived = one message arrived |
The event is not guaranteed per byte, and it is not on the UI thread |
Write returned = the peer finished processing |
In most cases it is closer to “the sender queued it into a buffer” |
| The COM list = the current truth of what is connected | Enumeration order is unspecified, and results can be stale |
For this reason, in serial communication you must define message boundaries yourself, as a protocol. Fixed-length frames, delimiter-based, length + payload + checksum — any shape is fine, but entering implementation with this left vague almost guarantees pain later.
3. What to Decide First
Before building a serial communication app, decide at least the items listed here up front.
3.1 Frame Boundaries
Decide which byte sequences count as one message. Fixed length? Newline-delimited? Length-prefixed? Is there a checksum / CRC? If this is vague, the receiver cannot tell “not enough yet” from “corrupted.”
3.2 Text, Binary, or a Mix
Decide up front whether it is an ASCII / UTF-8 line protocol, pure binary, or a mixture. Especially with mixtures like “the command part is a string, the payload is binary, only the tail has a newline,” the boundary collapses quickly unless you make explicit what gets decoded and from where bytes are treated as raw.
3.3 The Meaning of Each Timeout
Timeouts are safer thought of not as one value but separated by meaning.
- open timeout: until the port opens
- inter-byte timeout: time with no byte arriving mid-frame
- response timeout: from command issue to response completion
- reconnect backoff: the wait interval between reconnection attempts
Timeouts are stable when held not as “insurance against slowness” but as rules that advance the state machine.
3.4 Flow Control and Line State
The settings you want to make explicit are around here.
BaudRateDataBitsParityStopBitsHandshakeDTR/RTS
Settle this with “8N1 is roughly right” and, depending on the peer device, things will simply stop.
3.5 Separation of Responsibilities
Divide who is responsible for what.
- Who reads
- Who writes
- Who parses
- Who applies results to business state
Serial communication becomes more fragile the more the UI and the communication are mixed together.
3.6 Start / Stop / Reconnect State Transitions
At minimum, states like Closed, Opening, Ready, WaitingResponse, Fault, and Reconnecting should be part of the design. Right after an unplug/replug, the peer may still be booting, and there are times when you must not drag along the previous pending request.
3.7 Logging and Investigability
This is where the worst trouble comes later, almost always. At minimum, you want to record: open / close / reopen times, the port settings used, hex dumps of sent and received frames, checksum / CRC errors, frame timeouts / response timeouts, and the reason for each reconnect.
4. Common Pitfalls
4.1 Believing “One Read = One Message”
This is the most common. Say the peer returns a frame consisting of a header, length, payload, and CRC. If you call Read(buffer, 0, expectedLength) once and assume the return is one whole frame, partial reception breaks it easily.
The three usual breakage modes:
- Only the length arrived; the payload has not come yet
- One and a half frames arrived; the second half rolls into the next
Read - Two frames arrived together; only the first is processed and the rest is discarded
The countermeasure is simple: accumulate received bytes first, and let a parser carve frames out of the buffer.
4.2 Treating DataReceived Directly as a Business Event
.NET’s SerialPort.DataReceived looks convenient, but treating it as “a message has arrived” is dangerous. In practice, regard DataReceived as merely “something seems to have arrived,” do no heavy work inside the handler, and always marshal UI updates back to the UI thread.
4.3 Believing Anyone May Write from Anywhere
A configuration where the UI button, the monitoring timer, the reconnection logic, and the keepalive each call Write directly is fragile. Serial is a byte stream, so depending on the design, command interleaving or follow-up sends while awaiting a response can occur. Especially for request-response protocols and RS-485-style buses, leaning toward a single writer is substantially more stable.
4.4 Pushing Everything Through ReadLine() / WriteLine()
For a line-based text protocol, ReadLine() / WriteLine() are convenient. But they are convenient only when it truly is a line protocol. NewLine mismatches, newlines inside the payload, character-encoding differences, or mixed binary will break the boundary quickly.
4.5 Leaving Timeouts Undesigned, at Their Defaults
Drop in a careless synchronous read and you get a plain infinite wait. Worse, a configured timeout does not necessarily apply to every way of reading. Implementations that do synchronous reads on the UI thread, try to express everything with a single timeout, or just add retries, tend to wedge.
4.6 Taking RTS/CTS, XON/XOFF, and DTR/RTS Lightly
Handshaking and control lines matter a great deal against real hardware. With mismatched settings, the symptoms tend to be: transmission occasionally stalls, data is dropped beyond a certain volume, or behavior differs only right after opening. Some devices even interpret DTR/RTS transitions as boot or mode-switch signals.
4.7 Believing a Repeated Open() Equals Reconnection
Especially with USB-serial, it is entirely normal for the port to disappear temporarily, the old handle to become invalid, and the previous pending request to lose its meaning. Reconnection is safer handled as a bundle covering at least: invalidating the session, failing pending requests, stopping the reader / writer, reopening after backoff, and re-running device initialization.
4.8 Treating COM Port Enumeration as Truth
GetPortNames() is convenient, but appearing in the list and being openable are not the same. Blindly trusting last time’s COM7, auto-selecting the first enumeration result, or treating presence in the list as validity — these implementations cause operational trouble.
4.9 Thin Send/Receive Logs
TimeoutException, IOException, and Port closed alone tell you almost nothing. If you record send/receive timestamps, the port profile, hex dumps of traffic, parser errors, which request a response belongs to, and the trigger for each reconnect, triage advances considerably.
5. Best Practices
What pays off most is separating responsibilities.
reader: only reads bytes from the portwriter: only writes, in order, from the outbound queueparser: only carves frames out of the byte streamprotocol: handles request-response pairing and checksumsapp state: only updates business state
For reception, rather than treating each Read return as a business unit, the stable configuration accumulates into a buffer first and lets the parser carve out frames. Consolidating transmission into one worker — pushing the actual Write toward a single writer — reduces ordering slips.
For timeouts too, rather than settling on one number, separating them by meaning — open, inter-byte, response, reconnect — makes root-cause triage easier. Hold the port settings as a profile rather than ad-hoc code values, and log them at startup; on-site investigation becomes much easier.
Think of reconnection not as a mere reopen but as session regeneration. Rebuild everything — receive buffer, parser state, pending requests, the initialization sequence, and the readiness check — and you reduce the “breaks only occasionally” class of reconnection bugs.
Finally, we recommend keeping both raw logs and summary logs. Raw hex dumps and open / close history are strong for investigation; summaries of request IDs and retry counts are strong for operations.
6. The Checklist to Run First
- Are message boundaries written down explicitly?
- Is reception structured as byte accumulation → frame extraction?
- Are you treating
DataReceivedas message arrival? - Is there synchronous I/O on the UI thread?
- Is transmission a single writer?
- Are timeouts split by meaning rather than being one value?
- Are
Handshake/ DTR / RTS explicit? - Does reconnection rebuild the session?
- Are raw hex dumps recorded?
- Have you tested physical unplug/replug and mid-stream disconnection?
If several of these items look shaky, it is worth pausing once before going to production.
7. Summary
Finally, the key points one more time.
- Serial communication is a byte stream, not messages
Readunits and message units do not coincide- Boundaries must be defined as a protocol
- Treating
DataReceiveddirectly as a business event is fragile - Separate send/receive responsibilities, and push transmission toward a single writer
- Split timeouts by meaning, and design reconnection at the session level
- Logs that include raw hex dumps make later investigation much easier
In other words, in a serial communication app, how you interpret the byte stream and how you control time and state matters far more than opening the port. Just separating these concerns in the initial design substantially reduces the “breaks only occasionally” class of communication defects.
8. References
- Microsoft Learn,
SerialPort.DataReceivedEvent - Microsoft Learn,
SerialPort.ReadMethod - Microsoft Learn,
SerialPort.ReadTimeoutProperty - Microsoft Learn,
SerialPort.BaseStreamProperty - Microsoft Learn,
SerialPort.NewLineProperty - Microsoft Learn,
HandshakeEnum - Microsoft Learn,
SerialPort.DtrEnableProperty - Microsoft Learn,
SerialPort.RtsEnableProperty - Microsoft Learn,
SerialPort.GetPortNamesMethod - Microsoft Learn,
SerialPortClass - Microsoft Learn,
COMMTIMEOUTSstructure - Microsoft Learn,
DCBstructure - Microsoft Learn,
CreateFilefunction - pySerial API, Serial API Reference
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Windows App Outsourcing and Contract Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or contract development, here is how to sort out existing software modification, device inte...
System Tray Icons and Toast Notifications in Windows Apps — NotifyIcon Pitfalls and Choosing the Right AppNotification API
A practical guide to keeping a business Windows app resident in the system tray and notifying users with toast notifications. Covers the ...
How Long Will VB6 Apps Keep Running? — Runtime Support Status and a Practical Path to .NET Migration
How long will VB6 applications keep running? This practical guide clarifies the asymmetry between the VB6 runtime support policy (still s...
Japanese Era Dates, Public Holidays, and Closing-Date Processing in Business Apps — Era-Resilient Design, JapaneseCalendar, and Business-Day Calculations in Practice
A report that must show '令和8年' in the Japanese era, business-day calculations that exclude public holidays, payment due on the last busin...
More Than appsettings.json — A Practical Guide to Configuration Management in Windows Business Apps (Per-Environment Settings, Secrets, and Where to Write)
A practical look at configuration management for Windows business apps: layering appsettings.json, choosing between IConfiguration and th...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
Windows applications that include serial communication are more stable when designed end to end, covering receive handling, state transitions, reconnection, and UI separation.
Bug Investigation & Root Cause Analysis
This topic fits well with triaging communication failures such as occasional hangs, failure to recover only after a USB unplug/replug, and causality that cannot be traced from the logs.
Technical Consulting & Design Review
Sorting out protocol boundaries, flow control, timeouts, and single-writer design before implementation makes it easier to avoid defects with expensive rework.
Frequently Asked Questions
Common questions about the topic of this article.
- Why doesn't one Read call return one complete message in serial communication?
- Serial communication is an ordered byte stream, not a sequence of messages, so message boundaries never appear on their own. Data sent with a single Write may arrive in one Read, split across two, or concatenated with other data. Calling Read(100) does not guarantee exactly 100 bytes back. The reliable pattern is to accumulate received bytes into a buffer first and let a parser carve complete frames out of it, based on boundaries you define yourself as a protocol, such as fixed length, delimiters, or length plus payload plus checksum.
- Is the .NET SerialPort.DataReceived event reliable for detecting messages?
- No. DataReceived is not guaranteed to fire once per received byte, and it does not run on the UI thread, so treating it as a signal that one message has arrived is dangerous. In practice you should regard the event as merely indicating that something seems to have arrived, do no heavy work inside the handler, and always marshal UI updates back to the UI thread. Message detection itself belongs in a parser that works over an accumulation buffer.
- How many timeouts does a serial communication app actually need?
- One timeout value is not enough. Timeouts are more stable when separated by meaning: an open timeout for opening the port, an inter-byte timeout for gaps mid-frame, a response timeout from command issue to response completion, and a reconnect backoff interval between reconnection attempts. Thinking of them as rules that advance the state machine, rather than insurance against slowness, also makes root-cause triage much easier when something goes wrong.
- How should reconnection be handled with USB-to-serial adapters?
- Treat reconnection as session regeneration, not just calling Open() again. With USB-serial devices it is entirely normal for the port to disappear temporarily, the old handle to become invalid, and the COM number to change after re-enumeration. A robust reconnection bundle invalidates the session, fails pending requests, stops the reader and writer, reopens after a backoff, and re-runs the device initialization sequence. Rebuilding the receive buffer, parser state, and readiness check reduces the class of bugs that break only occasionally.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links