celld --listen vs --internal-listen: What's the Difference and When to Use Each

--listen controls the public worker-facing HTTP endpoint, while --internal-listen controls the peer-to-peer replication and operator command endpoint—two separate TCP sockets with distinct defaults and security requirements.

The celld daemon from denoland/celld exposes two independent listener flags that serve different traffic types. Understanding when each is required prevents configuration errors when deploying clusters or exposing nodes externally.

How the Two Listeners Work

Both flags use the same underlying Listen enum defined in crates/celld/startup.rs, but they populate separate fields in the Settings struct at crates/celld/main/cli.rs:15-17:

pub listen: Listen,
pub internal_listen: Listen,

The CLI parsing distinguishes them at crates/celld/main/cli.rs:48-60:

  • --listensettings.listen
  • --internal-listensettings.internal_listen

Default Behaviors

Flag Default Value Behavior
--listen 127.0.0.1:8080 (or AutoLoopback in managed mode) Explicit loopback address unless configured otherwise
--internal-listen LoopbackEphemeral (127.0.0.1:0) Random OS-assigned port on loopback

The Listen enum variants—AutoLoopback, LoopbackEphemeral, and Explicit—determine how each socket binds at startup.

What Each Listener Handles

--listen: Public Worker HTTP Interface

This endpoint receives worker runtime requests—the primary interface for executing JavaScript/TypeScript workloads. By default, it binds to 127.0.0.1:8080 for local development safety.

When you specify a non-loopback address (e.g., 0.0.0.0:8000 or a public IP), celld requires an explicit --internal-listen because:

  • The public socket cannot be reused for internal peer traffic
  • Internal replication traffic requires different authentication and access controls

--internal-listen: Peer and Operator Interface

This endpoint handles three specific traffic types:

  1. Peer-to-peer replication between celld nodes
  2. Unauthenticated operator commands (health checks, status queries)
  3. Internal cluster coordination

The default LoopbackEphemeral (127.0.0.1:0) is safe for single-node deployments where no external peers connect. For multi-node clusters, you must configure this explicitly.

Configuration Examples

Local Development (Defaults)

celld --bucket s3://my-fleet
  • Public listener: 127.0.0.1:8080
  • Internal listener: random loopback port (ephemeral)

External Worker Access with Secure Internal

celld --bucket s3://my-fleet \
      --listen 0.0.0.0:8000 \
      --internal-listen 127.0.0.1:9000
  • Workers connect via any interface on port 8000
  • Internal traffic stays on loopback, explicitly port 9000

Multi-Node Cluster with Advertisement

celld --bucket s3://my-fleet \
      --listen 192.168.10.5:8080 \
      --internal-listen 192.168.10.5:9090 \
      --advertise celld-node-1.internal:9090
  • Both listeners use explicit addresses
  • --advertise registers the internal listener for peer discovery

Critical Configuration Rules

Based on the source code in crates/celld/startup.rs and crates/celld/main/cli.rs:

  • Non-loopback --listen forces --internal-listen — The daemon will not start with an exposed public address unless internal is explicitly set
  • --advertise requires explicit internal address — You cannot advertise an auto-assigned ephemeral port
  • Authentication differs — The public listener requires worker authentication; the internal listener accepts unauthenticated operator commands (secured by network binding instead)

Key Source Files

File Purpose
crates/celld/main/cli.rs CLI flag parsing, Settings struct definition, help text
crates/celld/startup.rs Listen enum, default logic, socket binding implementation
crates/celld/main/cli.rs:42-46 User-facing help documentation describing flag semantics

Summary

  • --listen sets the worker-facing HTTP endpoint; defaults to 127.0.0.1:8080
  • --internal-listen sets the peer/operator endpoint; defaults to random loopback port
  • Non-loopback public addresses require explicit internal configuration
  • Both use the Listen enum but populate independent Settings fields
  • Multi-node deployments need explicit --internal-listen with --advertise

Frequently Asked Questions

What happens if I only set --listen to a public IP?

The daemon will fail to start with an error requiring --internal-listen. The source enforces this because peer traffic cannot reuse an externally reachable socket—internal operations must bind separately for security isolation.

Can I use the same port for both listeners?

No. Each flag creates an independent TCP socket. Even if you specify identical addresses, the second bind will fail with "address already in use." Use distinct ports (e.g., 8080 and 9090).

Why does --internal-listen default to a random port?

Single-node deployments don't need deterministic internal addressing—peers don't connect. The ephemeral default avoids port conflicts and simplifies local development. Production clusters require explicit ports for service discovery.

Is --internal-listen authenticated?

According to the help text and implementation, the internal listener accepts unauthenticated operator commands. Security relies on network binding (loopback or private networks) rather than application-layer authentication. This design prioritizes operational simplicity for health checks and debugging.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →