---
slug: agents/voice-agents
title: Voice agents
kind: howto
surface: agents
summary: Run a real-time voice agent on Aetherfy as an always-on dockerfile service on port 8080 (LiveKit worker, Pipecat on Daily) or on a sleeping service (Twilio Media Streams), with what the public URL exposes and which plans allow it.
sources:
  - docs-site/content/agents/dockerfile.mdx
  - docs-site/content/agents/aetherfy-yaml.mdx
  - docs-site/content/agents/quickstart.mdx
  - docs-site/content/agents/api-workspaces.mdx
  - docs-site/content/platform/limits.mdx
  - aetherfy-control-plane:orchestrator/fly_manager.py
  - aetherfy-control-plane:orchestrator/image_generator.py
  - aetherfy-control-plane:shared/idle_timeout.py
---

# Voice agents

A voice agent on Aetherfy is an ordinary `service` agent. What makes it
different is who calls whom: in the common setups your agent connects **out** to
a hosted media server, and nothing sends the Aetherfy agent HTTP requests. That
one fact decides the three settings every voice agent needs.

| Setting | Value | Why |
|---|---|---|
| `keep_alive` | `true` | Aetherfy's idle timer counts only incoming requests, so an Aetherfy agent that only connects out is suspended after `idle_timeout_minutes`. With `keep_alive: true` Aetherfy keeps one machine running and does not stop it when idle |
| `runtime` | `dockerfile` | The Python and Node runtimes have Aetherfy run the HTTP server and import your `app`; a LiveKit worker or a Pipecat bot is its own process, so the Aetherfy agent runs your container instead |
| Port | `8080` | An Aetherfy `dockerfile` service that does not listen on port 8080 never becomes healthy and the deploy fails: the supervisor answers `GET /health` with 503 while your server starts and 200 once it accepts connections on 8080 |

## What an Aetherfy voice agent needs from the network

An Aetherfy agent is reached through the edge at `<label>.aetherfy.dev`, over
HTTP and WebSocket. It does not receive incoming WebRTC or UDP.

| Setup | Works on Aetherfy | Why |
|---|---|---|
| LiveKit Agents worker, LiveKit Cloud or your own LiveKit server | Yes | The worker opens its connection out to LiveKit |
| Pipecat bot on the Daily transport | Yes | The bot joins the Daily room out from the machine |
| Pipecat bot on the LiveKit transport | Yes | The same, out to LiveKit |
| Twilio Media Streams | Yes | Twilio opens a WebSocket to your Aetherfy agent, and the edge passes WebSocket connections through |
| Pipecat `SmallWebRTCTransport` | **No** | It connects the caller's browser directly to the bot and needs incoming UDP, which the edge cannot carry. Use the Daily or LiveKit transport |

## Which Aetherfy plans allow a voice agent

An always-on voice agent needs two things, and both are plan features. Neither
is available on Free.

| Plan | `keep_alive: true` | `runtime: dockerfile` |
|---|---|---|
| Free | Not available | Not available |
| Starter | Available up to 2 GB, as a paid add-on | Available |
| Performance | Available up to 4 GB, as a paid add-on | Available |
| Enterprise | Included | Available |

The add-on is priced by memory: see [/platform/billing](/platform/billing) and
https://aetherfy.com/pricing. A voice agent that must not sleep is the case
`keep_alive` exists for. An agent without it is suspended when idle, and the
first request after that adds a short wait described in
[/agents/aetherfy-yaml](/agents/aetherfy-yaml).

## Run a LiveKit worker as an Aetherfy service

The Aetherfy manifest:

```yaml
runtime: dockerfile
type: service
memory_mb: 1024
regions:
  - us-east-1
keep_alive: true
```

A LiveKit worker runs a small HTTP server of its own for health checks. Its
default port in production is `8081`, which is not the port an Aetherfy agent
listens on, so set it in code:

```python
server = AgentServer(port=8080)
```

`AgentServer` takes `host` and `port` (the LiveKit Agents documentation, at
docs.livekit.io/agents/worker/options, read 2026-10-27, names no environment
variable for either). The same server answers `GET /` with `OK` once the worker
has registered with LiveKit, and with `503` before that. Aetherfy answers its own
`GET /health` from a supervisor and routes only to a machine whose port 8080
accepts connections, so the worker is healthy to Aetherfy as soon as that server
is up. With a wrong `LIVEKIT_URL` the deploy therefore goes healthy at first and
`GET /` answers `503`. The worker then retries its connection 16 times and exits
when they all fail (measured with a closed port), which stops the machine like any
server that exits after it started serving ([/agents/dockerfile](/agents/dockerfile)).
The Aetherfy agent's logs show `failed to connect to livekit`.

Start the worker with `python -m livekit.agents start agent.py` as the container's
`CMD`; `agents.cli.run_app` is the deprecated start form.

Put the LiveKit credentials in Aetherfy secrets, then redeploy, because secrets
reach a machine when it is built:

```bash
afy secrets set my-voice LIVEKIT_URL=wss://example.livekit.cloud LIVEKIT_API_KEY=key LIVEKIT_API_SECRET=secret
afy redeploy my-voice
```

## Run a Pipecat bot on the Daily transport as an Aetherfy service

Use the same manifest as above. The container must run a server on port 8080
that starts a bot for each call. Pipecat's development runner
(`python bot.py -t daily --host 0.0.0.0 --port 8080`) does that, but it serves
`POST /start`, `/client/` and `/docs` with no authentication, and the public
Aetherfy URL is open to anyone. Anyone who can reach it can start rooms and
spend your Daily and model credits. Write a small server of your own instead,
and give it these five properties:

- `POST /start` needs a bearer token from an Aetherfy secret, compared in
  constant time, and no other route, `/docs` included, is served.
- The server refuses to start, exiting with a message on stderr, when that token
  is missing or short. A server that exits before it listens fails the Aetherfy
  deploy and shows that message, which is better than serving an open `/start`.
- A cap on bots running at once, answered with `429` beyond it, sized to the
  agent's `memory_mb`: each bot loads its own voice-activity and turn-detection
  models.
- A bot that ends when its caller leaves, when it is ejected, and when nobody
  joins its room within a timeout.
- A room created for each call. Pipecat's helper reuses the room named in the
  `DAILY_ROOM_URL` environment variable when it is set, which would put every
  bot in one room.

The bot connects to the Daily room out from the machine, so the Daily transport
needs nothing inbound.

## Run Twilio Media Streams on an Aetherfy service that sleeps

A phone call does not need an always-on agent, because Twilio starts it with an
HTTP request:

1. Point the phone number's incoming-call webhook at your Aetherfy agent's
   public URL. That request is a real request, so it wakes a sleeping agent, and
   Aetherfy holds it until the agent is listening.
2. Answer with TwiML that connects the call to a WebSocket on the same agent:

   ```xml
   <Response>
     <Connect>
       <Stream url="wss://my-voice-k3m7x2q5hwd4nzra.aetherfy.dev/ws" />
     </Connect>
   </Response>
   ```

3. Twilio opens the WebSocket. An open WebSocket counts as activity for as long
   as it is held, so the agent stays up for the whole call and the idle window
   starts only when it closes.

The cost is the wake-up on the first ring of a call after a quiet period. Set
`keep_alive: true` if that matters to you. On the Node runtimes, export the
`http.Server` your WebSocket library is attached to, as described at
[/agents/node-and-bun](/agents/node-and-bun). Read `serves_websocket` on the
agent ([/agents/api-workspaces](/agents/api-workspaces)) to confirm Aetherfy
sees the route.

## What the public Aetherfy agent URL exposes

Everything your server serves, to anyone. An Aetherfy agent's key
authenticates you to Aetherfy, not your callers to your agent
([/agents/quickstart](/agents/quickstart)).

| Agent | What `https://<label>.aetherfy.dev` shows |
|---|---|
| LiveKit worker | `GET /` returns `OK` once the worker has registered with LiveKit and `503` before, and `GET /worker` returns the worker's SDK version and load as JSON. No one can start a call through it: calls arrive from LiveKit |
| Pipecat bot behind your own `/start` check | Only what you serve. Without the check, anyone can start a bot |
| Twilio webhook and WebSocket | Your webhook and `/ws`. Validate Twilio's request signature in the webhook and do not accept a WebSocket you did not hand out |

## Where the Aetherfy edge has not been measured

The edge's code relays a WebSocket upgrade, and the Aetherfy docs state that
WebSocket connections are passed through to your server
([/agents/dockerfile](/agents/dockerfile)). The edge's own measurements of
2026-08-31, however, did not measure a WebSocket upgrade end to end. Run one
real call before you depend on Twilio Media Streams in production.
