Open-source project
fastly/pushpin avatar
fastly/pushpin

Pushpin: a reverse proxy that adds WebSocket, HTTP streaming and long-polling to an existing API

A proxy server for adding push to your API, used at the core of Fastly's Fanout service

3,860 stars155 forksRustApache-2.0

At a glance

What is it?
Pushpin sits between clients and your backend, holds connections open, and lets you push data through a private HTTP control API. It is aimed at API creators who want realtime delivery without rewriting their stack.
Who is it for?
Adopt Pushpin if you already have an HTTP API and want WebSocket, streaming or long-polling without changing how your backend is written or deployed; the backend keeps answering short-lived requests and publishes through the control API at http://localhost:5561/publish/. Do not adopt it if you need a client-side SDK, per-user state held in the proxy, or a managed service, since Pushpin is transparent to clients and has no client libraries.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Pushpin solves for API creators

Most realtime stacks assume the server owns the connection. Pushpin inverts that. It is a reverse proxy placed in the network path between the backend and clients, and the backend keeps speaking ordinary short-lived HTTP. The README states the project is "unique among realtime push solutions in that it is designed to address the needs of API creators," and the design follows from that: no client library exists, because clients talk to Pushpin exactly as they would talk to the API.

The audience is teams that already run an HTTP API in any language and on any webserver, and want to add WebSocket, HTTP streaming or HTTP long-polling without adopting a new runtime. The backend never holds a socket open. It answers a request, and Pushpin decides whether that answer becomes a held connection based on the headers the backend returns.

How the proxy holds a connection and subscribes it to a channel

There are two integration points. First, the backend handles proxied requests. For plain HTTP each incoming request is proxied through. For WebSockets, the activity of each connection is translated into a series of HTTP requests sent to the backend, so the backend sees messages in batches rather than a live socket. Second, the backend tells Pushpin to push data, by making an HTTP POST to the private control API, which the README gives as `http://localhost:5561/publish/` by default.

The mechanism for streaming is a pair of response headers. The backend replies with `Grip-Hold: stream` and `Grip-Channel: test`. Pushpin consumes those headers, drops `Content-Length`, switches the client response to chunked transfer encoding, and keeps the client request open while the backend request is already finished. The connection is now subscribed to the channel named in `Grip-Channel`. Publishing to that channel injects data into every held connection on it.

That split is the whole architecture. The proxy owns connection lifetime and fan-out; the backend owns routing and business logic. It also means the backend can restart without disconnecting clients, which the README calls out explicitly for the WebSocket case: the handler is stateless and only runs when messages arrive.

A first streaming endpoint, step by step

The repository builds with Cargo and a postbuild step. The Makefile defines `build` as `cargo build`, and `postbuild` runs a second Makefile in the `postbuild/` directory to assemble the distributable layout. `make install` runs the postbuild install target. The README does not give a full from-source walkthrough, so treat the Makefile targets as the source of truth rather than a copy-paste recipe.

bash
make build
make install

The first command compiles the Rust code and then the postbuild step. The second installs the assembled files. The deb packaging metadata in `Cargo.toml` shows where things land on a Debian system: binaries under `usr/bin/`, the default config `dist/etc/pushpin/pushpin.conf` under `usr/share/pushpin/`, and the routes file under `etc/pushpin/routes`.

Once running, the first real use is a backend that answers with the Grip headers. The README's example response is short:

http
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 22
Grip-Hold: stream
Grip-Channel: test

welcome to the stream

Pushpin rewrites that into a chunked response to the client and leaves the connection open. The backend request is complete at this point. To send data, POST to the control API with the channel and a format block:

bash
curl -d '{ "items": [ { "channel": "test", "formats": { "http-stream": \
    { "content": "hello there\n" } } } ] }' \
    http://localhost:5561/publish

The client sees `hello there` appended to the stream it already has open. Nothing on the client side changed.

Using a backend library instead of hand-written headers

Writing Grip headers by hand works, but the README points to libraries for many backend languages and frameworks that set them for you. The Django example configures middleware and the control URI in `settings.py`:

python
MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

GRIP_PROXIES = [{'control_uri': 'http://localhost:5561'}]

A view then calls `set_hold_stream(request, 'test')` and returns a normal `HttpResponse`; the middleware adds `Grip-Hold` and `Grip-Channel` on the way out. Publishing uses `publish('test', HttpStreamFormat('hello there\n'))`. The Express path is similar: `ServeGrip` is instantiated with `control_uri` and a `key`, registered with `app.use`, and the publisher is taken from `serveGrip.getPublisher()`.

The WebSocket example is the more interesting one, because it shows where the abstraction leaks. The handler loops with `while (wsContext.canRecv())`, which looks like it runs for the life of the connection. It does not. The README says the loop is iterating a batch of WebSocket messages just received over HTTP, so the handler runs repeatedly, once per batch. If you read that loop as a persistent event loop, your mental model of the system will be wrong and your error handling will be too.

Where Pushpin is the wrong tool

Pushpin is transparent to clients, and that is a constraint as much as a feature. There are no client-side libraries, so anything requiring client logic beyond a stream, a poll or a WebSocket handshake has to be built by you on top of the raw connection. If you want a client SDK, presence tracking, or per-connection server state, this is not the layer that provides it.

The control API is another boundary. The README gives the default as `http://localhost:5561/publish/`, and the Express library example passes a `key` alongside `control_uri`. That implies the control API is meant to be reachable by your backend and not by the public internet; the documentation does not spell out a full security model for it, so treat network placement as your responsibility. Publishing is also a separate hop from serving requests, which means a backend that publishes on every request doubles its outbound traffic to the proxy.

Operationally, the proxy now sits in the path of every request, not just realtime ones. Anything in front of Pushpin that buffers chunked responses will break streaming, and the README does not document rollback or a degraded mode if the control API is unavailable. Long-polling and streaming both consume held connections, so connection limits on the proxy or the load balancer in front of it become a capacity question you have to answer yourself.

How Pushpin differs from running WebSockets in the application server

The obvious alternative is terminating WebSockets in the application server itself, which is what most frameworks support natively. The difference is where connection state lives. With a native WebSocket server, each application process holds its own sockets, so broadcasting to all clients means either sticky routing or a shared backplane such as Redis pub/sub, and a deploy drops connections. With Pushpin, the proxy holds the connections, the backend answers short-lived HTTP requests, and a restart of the backend does not disconnect anyone. That is the trade: you accept an extra network hop and a control API, and in exchange connection state moves out of your application.

A second alternative is a hosted realtime service. Pushpin is the core of Fastly's Fanout service according to the repository description, so a managed version exists, but the open source project is something you run and operate. If you do not want to run a proxy in the request path, the self-hosted route is not the right one for you.

Maintenance, upgrade cost and licence

The repository is not archived and the last push was on 2026-09-18, five days before this writing. Releases are not frequent: v1.42.0 landed on 2026-09-01, v1.41.0 on 2025-08-13, and v1.40.1 on 2024-08-14. That is roughly one release a year, so plan upgrades as occasional events rather than a continuous stream. The CHANGELOG.md at the repository root is where version-to-version changes are recorded; the README does not describe an upgrade procedure, and there is no documented rollback.

Build requirements are visible in `Cargo.toml`: edition 2018 and `rust-version = "1.81"`. The project is Rust plus C++, so a source build needs both toolchains, and `make fmt` invokes `clang-format` over files under `src/`. The Makefile also exposes `make check`, which runs `cargo test --all-features`, so there is a test target to run against your own changes.

The licence is Apache-2.0, declared in `Cargo.toml` and present as the LICENSE file. Apache-2.0 is a permissive licence with an explicit patent grant and requires that notices be preserved; if you redistribute a modified build, read the LICENSE and NOTICE requirements rather than assuming they are trivial. This is a description of the licence text, not legal advice.

Editorial conclusion

Adopt Pushpin if you already have an HTTP API and want WebSocket, streaming or long-polling without changing how your backend is written or deployed; the backend keeps answering short-lived requests and publishes through the control API at http://localhost:5561/publish/. Do not adopt it if you need a client-side SDK, per-user state held in the proxy, or a managed service, since Pushpin is transparent to clients and has no client libraries. Before committing, verify that your reverse proxy or load balancer forwards chunked responses without buffering, that the routes file maps the hosts you intend to expose, and that your backend framework can set Grip-Hold and Grip-Channel headers or has one of the listed backend libraries.

Frequently asked questions

How do I install Pushpin?

The repository builds with Cargo and a postbuild step: the Makefile defines `make build` as `cargo build` followed by the postbuild Makefile, and `make install` runs the postbuild install target. The README does not give a full from-source walkthrough, so check the Makefile and the packaging directory for your platform.

How do I use Pushpin with an existing HTTP API?

Put Pushpin in the network path between the backend and clients, then have the backend respond with `Grip-Hold` and `Grip-Channel` headers to turn a request into a held stream or long poll. To send data, POST to the control API at `http://localhost:5561/publish/` with the channel name and a format block.

Does Pushpin need a client-side library?

No. The README states Pushpin is transparent to clients and has no client-side libraries. Clients connect as they normally would, and Pushpin handles the held connection on the server side.

How does Pushpin handle WebSockets differently from a normal WebSocket server?

Pushpin converts WebSocket connection activity into a series of HTTP requests to the backend, so the backend handler runs once per batch of messages rather than holding a socket open. The README notes the handler is stateless and that restarting the app will not disconnect clients.

What does Pushpin's control API do?

It is the private endpoint the backend posts to in order to push data to clients, given as `http://localhost:5561/publish/` by default. Pushpin injects the posted data into any client connections subscribed to the named channel.

Official sources

  1. fastly/pushpin on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fastly-pushpin.svg)](https://hysenlabs.com/projects/fastly-pushpin)