# dunglas/vulcain: HTTP/2 Push and 103 Early Hints for Client-Driven REST APIs

> Vulcain is a protocol and a Caddy module that sits in front of an existing API and pushes related resources before the client asks for them. The trade-off is that the client has to declare its relations in a header, and the payoff only appears when the client would have fetched those relations anyway.

**dunglas/vulcain** — 🔨 Fast and idiomatic client-driven REST APIs.

- Repository: https://github.com/dunglas/vulcain
- Website: https://vulcain.rocks
- Stars: 3,595 · Forks: 103
- Language: Go
- License: AGPL-3.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/dunglas-vulcain

## The over-fetching and n+1 problem Vulcain attacks

A REST client that lists books and then needs the author of each book makes one request for the collection and one request per book. That is the n+1 problem, and the usual answers are GraphQL, JSON:API embedded resources, or sparse fieldsets. The README calls these "network hacks" for HTTP/1 and argues they cost you in HTTP cache behaviour, logs and security. Vulcain's position is that HTTP/2 and HTTP/3 already give you the tool: the server can send the related resources alongside the requested one, in parallel, over the same multiplexed connection.

The audience is therefore narrow and specific. It is for teams that already have a REST API, that want to keep the REST shape and the HTTP cache, and that control enough of the client to send a header describing which relations to pull. It is not a query language and it is not a data loader. The client still decides what it wants; the server just stops making it ask twice.

## How the Preload header turns into pushed responses

The mechanism is a request header named Preload whose value is a set of JSON Pointers into the response body. Given a collection at /books whose body contains a member array of IRIs, the client sends:

```http
GET /books/ HTTP/2
Preload: "/member/*/author"
```

The gateway reads the response it is about to return, resolves each pointer against the JSON, and finds the related resource identifiers. It then pushes /books/1, /books/2 and /authors/1 in addition to /books. Because HTTP/2 multiplexes streams, those pushed responses travel in parallel rather than in sequence. When the client later calls fetch() on the author link, the response is already in the push cache and is used immediately.

The README gives a JavaScript example of exactly that: a fetch to /books/1 with the header Preload: `"/author"`, then a second fetch on bookJSON.author that returns without a network round trip. There is also a query parameter form, preload, for clients that cannot set headers. The repository layout reflects the mechanism: traverse.go walks the JSON, json_pointer.go resolves the pointers, pusher.go emits the pushes, and openapi.go handles the case where relations are not expressed as IRIs.

## 103 Early Hints, preload links, and the fallback order

The protocol does not commit to one transport. The README states that Early Hints, the 103 status code from RFC 8297, is the preferred way to send the relations, and that Vulcain can fall back to preload links in the headers of the final response or to HTTP/2 Server Push when 103 is not supported. That ordering matters operationally. Early Hints can be emitted before the origin has finished generating the body, so the client starts fetching relations while the main response is still being built. Server Push has been deprecated in browsers, so the fallback chain exists because the transport layer is not stable across clients and intermediaries.

This is the part of the design that will bite you in production. A CDN or reverse proxy in front of the gateway has to pass the 103 through. If it does not, you silently fall back to link headers, which is a different performance profile rather than a failure, so you may not notice the degradation. The README does not document how to detect which path a given request took, and that gap is worth knowing about before you roll it out.

## Installing the Caddy module and making a first request

The gateway ships as a Caddy module, and the repository's Dockerfile shows the shape of the image: it starts from caddy:2-alpine, copies the built vulcain binary over /usr/bin/caddy, and copies a Caddyfile to /etc/caddy/Caddyfile. The README says a Docker image is provided and points to docs/gateway/caddy.md for the module and docs/gateway/install.md for the legacy standalone server. The repository also carries a legacy.Dockerfile and a Caddyfile at the top level.

A minimal Caddyfile that puts the gateway in front of an upstream API uses the vulcain directive and an upstream block naming the backend. The README does not print a full Caddyfile in the excerpt available, so check docs/gateway/caddy.md for the exact directive syntax before writing yours; the repository's own Caddyfile is the reference to copy.

```dockerfile
FROM caddy:2-alpine

COPY vulcain /usr/bin/caddy
COPY Caddyfile /etc/caddy/Caddyfile
```

Once the gateway is running in front of your API, the first real use is a request with the Preload header, as in the HTTP example above, followed by a client fetch on the relation. The thing to watch is the second fetch: if the push landed, it resolves from cache rather than from the network. If your API is not hypermedia, the README directs you to docs/gateway/openapi.md, where an OpenAPI specification documents the links between resources so the gateway can resolve string or integer identifiers that are not IRIs.

## Where Vulcain is the wrong tool

The client must cooperate. Vulcain only helps if the client sends Preload or the preload query parameter and then actually follows the links it declared. A mobile app that fetches exactly one resource and stops gains nothing and pays the cost of whatever the gateway pushed. Because the push decision is made from the response body, a client that over-declares relations causes the server to push resources nobody reads, which is wasted bandwidth rather than a correctness bug.

The second constraint is that relations have to be expressible. Hypermedia APIs get this for free, and the README recommends API Platform for building them. For an API whose related resource is a bare string or integer, you need an OpenAPI document to describe the links, which is an extra artifact to keep in sync with the API. If you cannot produce that document, the gateway has nothing to resolve against. The third constraint is transport: HTTP/1 clients get none of this, and the README's comparison document, docs/graphql.md, is where the project argues its case against GraphQL and other formats, so read it as advocacy rather than as a neutral survey.

## Vulcain against GraphQL and embedded-resource formats

GraphQL solves the same over-fetching and under-fetching problem by replacing the resource model with a query language and a single endpoint. The client sends a typed query describing the exact shape it wants, and the server resolves it, often through resolvers that fan out to the same data sources. Vulcain keeps the resource model and the URLs. The client sends pointers into a response it has not seen yet, and the server pushes whole resources that the HTTP cache can store under their own URLs.

That difference has consequences the README calls out: HTTP caching, logs and security. With Vulcain, each pushed resource is a normal cacheable response at its own URL, and your access logs still show per-resource requests. With GraphQL, the query is the cache key, and a POST body is not something a shared cache handles by default. The cost of Vulcain's approach is that the client has to know the relation paths, which is why the README points at the formal specification in spec/vulcain.md and at the Internet Draft. If you want a query language on top anyway, docs/graphql.md describes using GraphQL as the query language for Vulcain rather than as a replacement for it.

## Licence, maintenance and the cost of upgrading

Vulcain is licensed AGPL-3.0. That is a copyleft licence with a network clause, and it applies to the gateway server in this repository. If you run a modified gateway as a network service, the AGPL's source-availability condition is the thing your legal team will want to look at; this is not legal advice, and the LICENSE file at the repository root is the authoritative text. The protocol specification itself is published as an Internet Draft and maintained in spec/vulcain.md, which is a separate artifact from the Go implementation.

The last push to the repository was on 2026-08-15, and the most recent release listed is v1.4.2 on 2026-07-18. The module pins Go 1.26 in go.mod and depends on kin-openapi, gjson, sjson, zap and httpsfv, so an upgrade means tracking those. The Dockerfile builds on caddy:2-alpine and replaces the caddy binary with the vulcain one, which means the Caddy major version and the Vulcain build move together; a Caddy upgrade is a Vulcain rebuild, not a config change. Budget for that, and check the release notes for v1.4.0 through v1.4.2 before jumping.

## Conclusion

Adopt Vulcain if you already run a hypermedia API, or an API you can describe with an OpenAPI document, and your clients are browsers or HTTP/2 capable fetch clients that will follow the links you declare. Do not adopt it if your clients are HTTP/1 only, if you cannot express the relations between resources, or if your API is small enough that one extra round trip does not matter. Before committing, verify that your reverse proxy or CDN forwards 103 responses rather than swallowing them, and check the cache behaviour described in docs/cache.md, because a pushed response that is cached differently from the main response changes what your clients actually receive.

## FAQ

### What does Vulcain mean in this project?

The name is the project's own; the README does not give an etymology. It is used here for the protocol, the formal specification in spec/vulcain.md, and the gateway server that implements it.

### Is Vulcain a Go library I import, or a server I run?

Both shapes exist. The repository provides a Caddy web server module and a legacy standalone server, and the Go package is published at github.com/dunglas/vulcain/gateway according to the README badge.

### Does Vulcain work with a non-hypermedia API?

Yes. The README states that for non-hypermedia APIs, where the identifier of the related resource is a simple string or int, you use an OpenAPI specification to configure links between resources, documented in docs/gateway/openapi.md.

### How do I install Vulcain?

The README points to docs/gateway/caddy.md for the Caddy module and docs/gateway/install.md for the legacy standalone server, and states that a Docker image is provided. The repository's Dockerfile starts from caddy:2-alpine and copies the vulcain binary over /usr/bin/caddy.

## Sources

- [dunglas/vulcain on GitHub](https://github.com/dunglas/vulcain)
- [License: AGPL-3.0](https://github.com/dunglas/vulcain/blob/main/LICENSE)
- [Project website](https://vulcain.rocks)
- [README](https://github.com/dunglas/vulcain/blob/main/README.md)
- [Releases](https://github.com/dunglas/vulcain/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dunglas-vulcain
