# Native TLS Encryption for bunqueue TCP & HTTP

Encrypt bunqueue TCP and HTTP traffic with native TLS, no reverse proxy needed. Server cert/key setup, client options, CLI flags, self-signed certs.

Canonical: https://bunqueue.dev/guide/tls/

---

import { Tabs, TabItem } from '@astrojs/starlight/components';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">server · tls</span>
  <h1 class="bq-hero-h1 bq-bench-h1">TLS without the <em>reverse proxy.</em></h1>
  <p class="bq-hero-sub">Point bunqueue at a certificate and key, and all traffic between clients and the server is encrypted. No nginx or Caddy in front, one cert pair covers both the TCP and HTTP ports.</p>

  <div class="bq-proof">
    <span><b>1</b> cert pair covers TCP and HTTP</span>
    <span><b>0</b> reverse proxies needed</span>
    <span><b>fail-fast</b> startup on partial config</span>
  </div>
</div>

TLS (the encryption behind `https://`) is opt-in: without a cert and key the server runs in plaintext, exactly as before. Turn it on whenever clients connect over a network you don't fully trust.

## Enable TLS on the server

```bash
# CLI flags
bunqueue start --tls-cert ./cert.pem --tls-key ./key.pem

# Or environment variables
TLS_CERT_FILE=./cert.pem TLS_KEY_FILE=./key.pem bunqueue start
```

Or in `bunqueue.config.ts`:

```typescript
import { defineConfig } from 'bunqueue';

export default defineConfig({
  server: {
    tlsCertFile: './cert.pem',
    tlsKeyFile: './key.pem',
  },
});
```

One cert pair covers both servers: TCP (`:6789`) and HTTP/WebSocket/SSE (`:6790`, which becomes `https://` / `wss://`).

The server fails fast at startup if the cert or key file is missing, or if only one of the two is set. It never silently falls back to plaintext.

## Connect a client

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
import { Queue, Worker } from 'bunqueue/client';

// Public CA (Let's Encrypt etc.): verify with system CAs
const queue = new Queue('jobs', {
  connection: { host: 'queue.example.com', port: 6789, tls: true },
});

// Private CA or self-signed: trust a specific CA file
const queue2 = new Queue('jobs', {
  connection: { host: '10.0.0.5', port: 6789, tls: { caFile: './ca.pem' } },
});

// Dev only: skip verification
const queue3 = new Queue('jobs', {
  connection: { host: 'localhost', port: 6789, tls: { rejectUnauthorized: false } },
});
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
import { Queue, Worker } from 'bunqueue-client';

// Public CA (Let's Encrypt etc.): verify with system CAs
const queue = new Queue('jobs', { host: 'queue.example.com', port: 6789, tls: true });

// Private CA or self-signed: trust a specific CA file
const queue2 = new Queue('jobs', { host: '10.0.0.5', port: 6789, tls: { caFile: './ca.pem' } });

// Dev only: skip verification
const queue3 = new Queue('jobs', { host: 'localhost', port: 6789, tls: { rejectUnauthorized: false } });
```

</TabItem>
<TabItem label="Python">

```python
from bunqueue import Queue

# Public CA (Let's Encrypt etc.): verify with system CAs
queue = Queue("jobs", host="queue.example.com", port=6789, tls=True)

# Private CA or self-signed: trust a specific CA file
queue2 = Queue("jobs", host="10.0.0.5", port=6789, tls={"ca_file": "./ca.pem"})

# Dev only: skip verification (a preconfigured ssl.SSLContext also works)
queue3 = Queue("jobs", host="localhost", port=6789, tls={"verify": False})
```

</TabItem>
<TabItem label="PHP">

```php
use Bunqueue\Queue;

// Public CA (Let's Encrypt etc.): verify with system CAs
$queue = new Queue('jobs', ['host' => 'queue.example.com', 'port' => 6789, 'tls' => true]);

// Private CA or self-signed: trust a specific CA file
$queue2 = new Queue('jobs', ['host' => '10.0.0.5', 'port' => 6789, 'tls' => ['caFile' => './ca.pem']]);

// Dev only: skip verification
$queue3 = new Queue('jobs', ['host' => 'localhost', 'port' => 6789, 'tls' => ['verifyPeer' => false]]);
```

</TabItem>
<TabItem label="Go">

```go
// Public CA (Let's Encrypt etc.): verify with system CAs
queue := bunqueue.NewQueue("jobs", bunqueue.Options{
    Host: "queue.example.com", Port: 6789,
    TLS:  &bunqueue.TLSOptions{},
})

// Private CA or self-signed: trust a specific CA file
queue2 := bunqueue.NewQueue("jobs", bunqueue.Options{
    Host: "10.0.0.5", Port: 6789,
    TLS:  &bunqueue.TLSOptions{CAFile: "./ca.pem"},
})

// Dev only: skip verification
queue3 := bunqueue.NewQueue("jobs", bunqueue.Options{
    Host: "localhost", Port: 6789,
    TLS:  &bunqueue.TLSOptions{InsecureSkipVerify: true},
})
```

</TabItem>
<TabItem label="Rust">

```rust
use std::path::PathBuf;
use bunqueue_client::{ConnectionOptions, Queue, TlsOptions};

// Public CA (Let's Encrypt etc.): verify with system CAs
let queue = Queue::new("jobs", ConnectionOptions {
    host: "queue.example.com".into(),
    tls: Some(TlsOptions::default()),
    ..Default::default()
});

// Private CA or self-signed: trust a specific CA file
let queue2 = Queue::new("jobs", ConnectionOptions {
    host: "10.0.0.5".into(),
    tls: Some(TlsOptions { ca_file: Some(PathBuf::from("./ca.pem")) }),
    ..Default::default()
});
```

*Rust deliberately exposes no insecure TLS mode, verification is always on. For a self-signed cert, trust it through `ca_file`.*

</TabItem>
<TabItem label="Elixir">

```elixir
# Public CA (Let's Encrypt etc.): verify with system CAs
queue = Bunqueue.queue("jobs", host: "queue.example.com", port: 6789, tls: true)

# Private CA or self-signed: trust a specific CA file
queue2 = Bunqueue.queue("jobs", host: "10.0.0.5", port: 6789, tls: true, ca_file: "./ca.pem")

# Dev only: skip verification
queue3 = Bunqueue.queue("jobs", host: "localhost", port: 6789, tls: true, verify: false)
```

</TabItem>
</Tabs>

Workers accept the same TLS options as queues in every SDK (in the Bun client, under `connection.tls`). The wire protocol is unchanged, TLS only wraps the transport.

From the CLI:

```bash
bunqueue stats --host queue.example.com --tls    # system CAs
bunqueue stats --tls-ca ./ca.pem                 # custom CA
bunqueue stats --tls-no-verify                   # self-signed, dev only
```

## Self-signed certificate (dev / internal networks)

No public domain? Generate your own cert:

```bash
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -keyout key.pem -out cert.pem \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
```

Clients then connect with the CA-file option pointing at `cert.pem` (`caFile` in TypeScript and PHP, `ca_file` in Python, Rust, and Elixir, `CAFile` in Go): the self-signed cert acts as its own CA. That keeps full verification, no verification opt-out needed.

## Good to know

- Certificate verification is on by default in every SDK: the client rejects untrusted or mismatched server certs unless you explicitly opt out (`rejectUnauthorized: false` in TypeScript, `{"verify": False}` in Python, `['verifyPeer' => false]` in PHP, `InsecureSkipVerify: true` in Go, `verify: false` in Elixir), encryption without authentication, dev only. Rust exposes no insecure mode at all.
- TLS encrypts traffic but does not identify clients. Combine it with [auth tokens](/guide/env-vars/) for servers exposed beyond localhost.
- A TLS-enabled server only accepts TLS clients; plaintext clients fail the handshake immediately (they do not hang).
- HTTP endpoints (`/health`, dashboards, `/ws`, `/events`) are served over `https://`/`wss://` when TLS is enabled.