Press Enter in a text editor and, depending on the machine, you may have just written two characters into the file rather than one: carriage return (CR, 0x0D) followed by line feed (LF, 0x0A). Nothing about a modern screen requires two. The reason is a machine almost nobody has touched since the 1970s.
Two mechanical jobs, two codes
The Teletype Model 33, introduced in 1963, was the terminal that ASCII was designed around. It printed onto a paper roll with a print head that travelled along a carriage. Ending a line meant doing two physically distinct things: slide the carriage back to the left margin, and roll the paper up by one line. Those were separate mechanisms driven by separate solenoids, so ASCII gave them separate control codes. CR moved the carriage. LF advanced the paper.
Keeping them separate was not an oversight — it was useful. Sending LF without CR gave you a new line at the current column, which is how you produced tables and indented output. Sending CR without LF re-printed over the line you had just typed, which is how early terminals did overstrike, bold, and underline. And the carriage return itself was slow: sliding a physical print head across the platen took longer than the time budgeted for one character at 110 baud, so software commonly padded the sequence with a NUL or two to give the mechanism time to finish before the next printable character arrived. Send LF first and the next line would begin printing mid-sweep, smeared across the page.
Why the split survived the hardware
When operating systems moved off paper, each one picked a convention and froze it.
Multics used LF alone as a line terminator; Unix inherited that decision, and so did Linux, BSD, and eventually macOS. CP/M kept the CR+LF pair because it was written for teletype-style hardware, MS-DOS copied CP/M, and Windows has carried CR+LF ever since. Classic Mac OS through Mac OS 9 went the third way and used CR alone. Three families, three answers, all still in circulation as file artefacts.
The network protocols made the more consequential choice. HTTP, SMTP, FTP, and IRC all specify CR+LF as the line terminator in their grammars, and they still do. A protocol has to be unambiguous across every operating system that will ever speak it, so the specification names the exact octets rather than deferring to a local convention. Every HTTP header you have ever sent ended with two bytes chosen because of a printing mechanism.
The part that costs embedded teams an afternoon
In IoT and embedded work, line endings stop being trivia and become a bug class. A few places it reliably bites:
AT commands. Cellular and Wi-Fi modules — SIM7600, quectel modems, the classic ESP-AT firmware — parse commands terminated with CR+LF. Send AT+CSQ with a bare newline and the module does not error, it simply does not answer, because as far as its parser is concerned the command has not ended yet. The symptom is a timeout, which sends people hunting for a wiring or baud-rate fault that is not there.
Serial logs that print a staircase. A device that emits only LF looks fine in most IDE serial monitors, because the monitor translates on your behalf. Open the same port in a raw terminal, or pipe it to a file and open it on a machine that does no translation, and every line starts where the last one ended — descending diagonally down the screen. The device is not broken; the terminal is doing exactly what LF means without an accompanying CR.
Invisible trailing bytes in config files. A settings file, a CSV of calibration values, or a provisioning list edited on Windows and parsed on a Linux gateway carries a CR at the end of every line. String comparisons fail against values that look identical on screen. Numeric parsers may succeed, hiding the problem until a field that happens to be compared as text — a device ID, an MQTT topic, an API key — silently stops matching. This is worth a linter rule and a .gitattributes entry rather than a debugging session.
Protocol framing on a UART. If your own device-to-device protocol delimits messages by newline, decide which bytes terminate a frame and write it in the spec, not in the reader's head. A parser that splits on LF and leaves the CR attached to the previous field will work perfectly against one sender and mysteriously fail against another.
The practical rule
Be strict in what you send and lenient in what you accept. Emit CR+LF when talking to anything that specifies it — modems, HTTP, SMTP, most serial terminals — and when parsing, strip both characters from the end of every line regardless of which arrived. Two lines of defensive trimming at the edge of the firmware removes an entire category of field failure that is nearly impossible to see once the device is installed somewhere inconvenient.
It is also a reasonable illustration of how embedded systems age. The Model 33 is a museum piece. Its carriage timing is still in your HTTP stack, your modem driver, and your serial console, because compatibility is cheaper to keep than to break — which is exactly why the odd corners of a protocol usually turn out to have a mechanical explanation rather than an arbitrary one.
If you are building connected hardware and want the serial, protocol, and firmware layers to hold up in the field, talk to us about your project or see what we build.
Top comments (0)