TCP Lands: Luma Opens a Socket to the World

TCP Lands: Luma Opens a Socket to the World

February 11, 2026·
Luma Core Team

Three tools. Three posts. Today, the last one lands.

We shipped binary buffers for building and parsing packets. We shipped crypto for hashing and authentication. Now Luma gets TCP networking — the transport layer that connects everything to everything else.

With TCP, a Luma program can open a socket, send bytes, read responses, upgrade to TLS, and close the connection. That’s enough to speak any protocol: MySQL, PostgreSQL, Redis, SMTP, HTTP, or anything else that runs over TCP.

Connecting

sock: tcp = tcp.connect("localhost", 3306)

One line. Connects to the given host and port. Panics if the connection fails — same convention as file().

When failure is expected, use the optional type:

sock: ?tcp = tcp.connect("db.example.com", 3306, { timeout: 3 })

if sock == nil {
    print("Cannot reach database server")
}

Optional connect returns nil instead of panicking. Use it for failover logic, health checks, or any situation where a connection might not be available.

Options

The third argument is an options map with three fields:

sock: tcp = tcp.connect("host", 3306, { timeout: 5, read_timeout: 10, tls: true })

timeout — connection timeout in seconds. How long to wait for the initial TCP handshake. Default: system default.

read_timeout — read timeout in seconds. Applies to every subsequent read() and read_line() call for the lifetime of the connection. Default: no timeout.

tls — when true, performs a TLS handshake immediately after connecting. Use this when the remote service expects encryption from the start (HTTPS, SMTPS, MySQL with require_secure_transport).

All options are optional. Use only what you need:

sock: tcp = tcp.connect("host", 3306, { timeout: 5 })
sock: tcp = tcp.connect("host", 443, { tls: true })

Writing

sock.write(data)

Duck-typed on the argument. The compiler sees what you’re passing and dispatches:

sock.write("QUIT\r\n")                  // str — sends UTF-8 bytes
sock.write(packet_bytes)                // [byte] — sends raw bytes

No write_str or write_bytes. If it’s a string, Luma sends the string. If it’s bytes, Luma sends the bytes.

Writes always panic on error — if the connection is broken, continuing makes no sense.

Reading

Two methods, each with return-type dispatch.

read(n) — exact bytes

data: [byte] = sock.read(4)       // 4 bytes as byte list
text: str = sock.read(1024)       // 1024 bytes as string

Blocks until exactly N bytes have been received. This is the primary mode for protocol implementations where packet sizes are known — a 4-byte MySQL header, a payload of known length, a fixed-size handshake field.

The variable declaration tells Luma what format you want. Same call, different return type:

header: [byte] = sock.read(4)     // bytes
greeting: str = sock.read(64)     // string

read_line() — until newline

line: str = sock.read_line()

Reads bytes until \n is encountered. Returns the full line including the newline. This is a genuinely different operation from read(n) — you don’t know how long the line is, so you can’t use exact-byte reads.

Used for text-based protocols: SMTP, HTTP headers, Redis.

Optional reads

Every read variant supports optional types for graceful error handling:

data: ?[byte] = sock.read(4)
text: ?str = sock.read(1024)
line: ?str = sock.read_line()

Returns nil on connection error or timeout instead of panicking. Use this when you’re reading from unreliable connections or when timeout is set and expected to fire.

TLS

Two modes: connect-time and mid-conversation.

TLS from the start

sock: tcp = tcp.connect("smtp.gmail.com", 465, { tls: true })

The tls option performs the TLS handshake as part of connection setup. One option, no extra method call.

STARTTLS upgrade

Some protocols start unencrypted and upgrade mid-conversation. MySQL does this. SMTP does this. LDAP does this.

sock: tcp = tcp.connect("localhost", 3306)
// ... unencrypted handshake ...
sock.start_tls()
// ... now everything is encrypted ...

After start_tls(), all reads and writes go through the encrypted channel. The transition is seamless — the same sock variable, the same methods, just encrypted now.

Connection lifecycle

sock.close()

Closes the connection. Safe to call multiple times — the second call is a no-op. After closing, any read or write will panic.

What this looks like in practice

Reading a MySQL server greeting

The simplest proof that TCP works — connect to MySQL and read the handshake greeting it sends immediately after connection:

sock: tcp = tcp.connect("localhost", 3306)

// MySQL sends a greeting packet on connect
// Format: [3 bytes length][1 byte seq][payload]
header: [byte] = sock.read(4)

// Parse the 3-byte little-endian length
buf: buffer = buffer(header)
length: int = buf.read(3).to_int24()

// Read the payload
payload: [byte] = sock.read(length)

// First byte is protocol version (always 10)
print("Protocol version: ${payload[0]}")

sock.close()

Notice how TCP, buffers, and byte indexing work together. TCP reads the raw bytes. The buffer parses the length field. Standard array indexing gets the protocol version. Each tool does one thing.

Simple HTTP client

TCP isn’t just for databases. Any text protocol works:

sock: tcp = tcp.connect("example.com", 80)

sock.write("GET / HTTP/1.1\r\n")
sock.write("Host: example.com\r\n")
sock.write("Connection: close\r\n")
sock.write("\r\n")

status: str = sock.read_line()
print("Status: ${status}")

sock.close()

Safe connection with failover

sock: ?tcp = tcp.connect("primary.example.com", 5432, { timeout: 2 })

if sock == nil {
    print("Primary down, trying backup...")
    sock = tcp.connect("backup.example.com", 5432)
}

data: [byte] = sock.read(1024)
sock.close()

HTTPS request

sock: tcp = tcp.connect("example.com", 443, { tls: true, read_timeout: 5 })

sock.write("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")

status: str = sock.read_line()
print(status)

sock.close()

Error messages that help

TCP errors include context about what went wrong:

  • TCP connect failed: connection refused (localhost:3306)
  • TCP read failed: timeout after 5s
  • TCP write failed: broken pipe
  • TLS handshake failed: certificate verify failed

When a connection fails at 3 AM, these messages tell you what happened without reaching for Wireshark.

Feature-flagged, as always

Like buffers and crypto, the TCP runtime is only included when you use it. A program that never touches tcp.* gets no networking imports, no TCP struct, no extra binary size. The compiler detects usage at compile time and emits only what’s needed.

The toolbox is complete

All three low-level primitives from our database roadmap are done:

Primitive Status What it enables
Binary buffers Done Packet construction and parsing
Crypto Done Authentication, integrity, tokens
TCP networking Done Socket connections to databases and services

With these three, we have everything needed to write a MySQL driver in pure Luma. Opening a socket. Performing the handshake. Authenticating with caching_sha2_password. Sending queries. Parsing results. All in regular .luma files that anyone can read, understand, and modify.

What we shipped

  • tcp.connect(host, port) — plain TCP connection
  • tcp.connect(host, port, { tls: true }) — TLS connection
  • tcp.connect(host, port, { timeout: n }) — connection timeout
  • tcp.connect(host, port, { read_timeout: n }) — per-connection read timeout
  • Optional connect?tcp returns nil on failure
  • sock.write(data) — duck-typed for str and [byte]
  • sock.read(n) — exact byte read, return-type dispatched ([byte], str, ?[byte], ?str)
  • sock.read_line() — read until newline (str, ?str)
  • sock.start_tls() — mid-connection TLS upgrade (STARTTLS)
  • sock.close() — idempotent connection close

13 new .lcore runtime files. Zero external dependencies. Everything compiles from Go’s standard library.

The toolbox is built. Now we build what it was for.

Last updated on