Cut your coding agent’s network traffic.
Every turn, Claude Code, Codex and other coding agents resend the whole growing conversation. Portway compresses each request against the previous one, so only the difference travels to a Portway receiver, which restores the original and forwards it to the model.
brew install rath/tap/portway
macOS Apple silicon · Linux x86_64 / ARM64
- Sent by the agent
- 13,877,261 B
- zstd alone
- 3,114,150 B
- Portway on the wire
- 101,551 B
Turn 40: the agent sent 429,900 bytes and 1,029 crossed the wire.
Watch the bytes shrink
Portway comes with a terminal dashboard (portway --tui) and a browser console (--web). Both show each request as it leaves: what the agent sent, what crossed the network, how long it took and how many tokens it carried. These screens were captured from a real Claude Code and Codex session.
Install, then connect your agent
The Homebrew package includes the terminal dashboard and the browser console; no Rust toolchain is needed.
Prefer guided setup? The Portway plugin for Codex and Claude Code can configure your client and check its connection. Install or upgrade to Portway 0.2.5 or later with Homebrew or a release download; no source build is needed. The plugin only adds the setup and doctor skills; it does not route traffic. After setup, Codex goes through Portway only when started with codex -p portway, while Claude Code goes through it in every new session, because setup writes its user settings.
-
Install Portway. Supports macOS Apple silicon and Linux x86_64 / ARM64 (glibc 2.28 or later).
brew install rath/tap/portwayWithout Homebrew? Download a release or build from source. Only source builds need Rust and a C toolchain.
-
Save your API mounts as
portway.toml. Then start Portway with its browser console:# portway.toml host = "127.0.0.1" port = 8787 [upstreams] anthropic = "https://api.anthropic.com" codex = "https://chatgpt.com/backend-api/codex"portway --config ./portway.toml --web -
Connect Claude Code in a second terminal. First install Claude Code and sign in normally. Leave Portway running, then use this command for one invocation:
env ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic claude -
Connect Codex with a separate profile. First install Codex and sign in with ChatGPT using
codex login. Save the following as~/.codex/portway.config.toml(insideCODEX_HOMEif you set it):model_provider = "portway" [model_providers.portway] name = "portway" base_url = "http://127.0.0.1:8787/codex" requires_openai_auth = true wire_api = "responses"Then run this from your project directory:
codex -p portwayThe profile inherits your usual model and preferences. Run plain
codexto use your usual provider. Both clients keep their credentials; Portway needs no API key of its own. Do not append/v1to either mount URL. -
Check your first request. Send a short message in your CLI and confirm it answers and appears in Portway’s log or browser console. These direct provider connections do not enable delta compression. If you already use a remote Portway server, replace
127.0.0.1:8787in the client settings with its reachable address. -
Add a receiver to enable delta compression. Run
portway receivebehind the gateway in front of your service, then point the sender at the gateway. Receiver setup walks through it.
The same repository ships portway-core, an embeddable Rust library, and a CLI
with daemon mode, reports, a terminal dashboard and a browser console. It records request
metadata in SQLite, never bodies, credentials or dictionary contents.
Turn 40 is turn 39 plus four kilobytes
A coding agent keeps no state on the server. To take one more step, it sends the system prompt, every earlier message and every tool result again, followed by the few kilobytes that are new. Each request is almost entirely a copy of the one before it.
- Turn 1263,966 B
- Turn 2268,145 B
- Turn 3272,556 B
- Turn 20344,723 B
- Turn 39425,707 B
- Turn 40429,900 B
So the upload of a session grows with the square of its length. Where that upload is what you wait for or pay for, it adds up quickly:
- a self-hosted model server, or a gateway you run, in another region;
- a remote development machine, where every turn crosses a VPN or an SSH hop;
- a laptop on a slow, shared or metered connection.
Short, single-shot requests gain little. Nothing earlier can serve as a base.
The previous turn becomes the dictionary
Portway runs twice: a sender next to your agent, and a receiver in front of the service it calls. Both keep the last request body they agreed on. The sender compresses the next request against it, and the receiver rebuilds the original before your service sees a byte.
- AgentClaude Code, Codex, any client with a base URL
- 268,145 Blocalhost
- portwaysender, 127.0.0.1:8787
- 1,054 Bthe slow link
- portway receiveon a host you control
- 268,145 Bsame datacentre
- Your servicea model server, a gateway, or a provider API
How the two ends agree on a base
-
Turn 1 seeds the dictionary. It travels zstd-compressed with
X-Dict-Store: 1. The receiver decodes it, passes it on and keeps the body. -
The receiver confirms what it kept. Its response carries
X-Dict-Storedwith the SHA-256 of the bytes it actually stored. The sender uses a body as a base only when that hash matches its own copy. -
Turn 2 names turn 1 by hash. It travels as DCZ: a 40-byte header carrying the dictionary's SHA-256, then a checksummed zstd frame compressed against it. The sender keeps up to eight confirmed bodies per route and picks the one that shares the longest prefix with the new request. In an append-only conversation, that is the previous turn.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x5E | 0x2A | 0x4D | 0x18 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x20 | 0x00 | 0x00 | 0x00 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ SHA-256 of the dictionary, 32 bytes +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ zstd frame compressed against the dictionary, +
| ending in a content checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
When a base goes missing, it falls back
A receiver restarts, evicts an old body, or meets a sender it has never seen. None of that fails a request. The receiver refuses what it cannot restore before your application runs, marks the refusal, and the sender tries again with less.
-
DCZ against a stored body
Falls back on
412dictionary gone, or400or415frame refused -
Plain zstd, no dictionary
Falls back on
415encoding refused - Uncompressed, exactly as the agent sent it Always accepted
A request is sent at most three times. Portway only replays a refusal carrying the receiver's marker, and the receiver strips that marker from application responses, so your application cannot trigger a replay and never runs a request twice. Errors from your application come back to the agent unchanged.
A turn costs what it added
The same benchmark at four context sizes, twenty turns each. Plain zstd shrinks each request by about four fifths; the previous-turn dictionary leaves about a kilobyte, whatever the size of the context.
| Context at turn 1 | Uncompressed | zstd only | Portway | Saved over 20 turns |
|---|---|---|---|---|
| 128 KiB | 175,405 | 41,962 | 1,104 | 98.5% |
| 256 KiB | 306,503 | 69,608 | 1,058 | 98.7% |
| 512 KiB | 570,438 | 122,089 | 1,034 | 98.8% |
| 1 MiB | 1,094,276 | 222,937 | 1,048 | 98.9% |
The last column includes the first request, which carries the whole context to seed the dictionary. The synthetic text is tuned so that plain zstd saves about as much as it does on Portway's own source. Every run is reproducible byte for byte, and every restored body is checked against the SHA-256 of what was sent.
cargo build --release --locked
python3 scripts/bench.py --turns 20 --context-kb 1024
What it does not do
- It saves bytes, not tokens.
- The model receives the identical request and bills the identical tokens. Provider prompt caching cuts the cost of processing a repeated prefix; Portway cuts the cost of sending it. Use both.
- It needs a receiver.
- Pointed straight at an API that does not advertise the protocol, the sender forwards
requests unchanged and uncompressed. The receiver is
portway receive, or any server that implements the protocol. - It forgets on restart.
- Dictionaries live only in memory on both ends, kept apart per credential. By default a body under 32 KiB never becomes one.
- It speaks plain HTTP/1.1.
- No WebSockets, CONNECT, HTTP/2 or inbound TLS; put your gateway in front for TLS and authentication. Requests are buffered up to a configurable 256 MiB, and responses stream.