Connect a source inside your network.

The Perfloop relay is a small program you run inside your network so Perfloop can read telemetry the internet cannot reach. It reads Prometheus and VictoriaMetrics for metrics, Loki for logs, and Go pprof for live profiles of Go processes. It opens outbound connections to Perfloop and answers the reads Perfloop has already validated. It collects nothing.

Updated 1 October 2026.

The relay is open source under the Apache License 2.0 at github.com/perfloop/relay. The image Perfloop publishes is built from that repository, so you can read what runs in your network.

Access you need

  • relay tokenA Perfloop credential with the single scope relay, which the server sets. It opens relay tunnels for your account. Perfloop refuses it on every other API route, so it cannot read stored results or anything else from Perfloop. Whoever holds it receives the validated queries, time windows, and read ids Perfloop sends down a tunnel and can answer them, with false telemetry if stolen, until you revoke it. Keep it in a secret store. An admin creates and revokes it in Setup.
  • outbound HTTPSThe relay dials app.perfloop.ai on port 443 and accepts no inbound connection. If your network requires an egress proxy, set HTTPS_PROXY in the relay's own environment, for example with -e HTTPS_PROXY on docker run or an env entry in the Deployment; it must carry WebSockets. Requests to your providers never use it.
  • provider read credentialA read-only credential for each provider, kept in the relay configuration inside your network. The relay sends it only to that provider. Perfloop refuses a credential in Setup for a relay source.

Setup

For a checkpoint after each step and a table of what each failure means, see docs/setup.md in the repository. This page describes the Setup form as it is.

1 · choose the image

The repository's Image workflow publishes ghcr.io/perfloop/relay tagged with the commit it built, on each push to main. The package is public, so a pull needs no credential. Pick the commit of the latest successful Image run on main, listed under Actions in the repository, and resolve its digest.

docker buildx imagetools inspect ghcr.io/perfloop/relay:<commit sha> \
  --format '{{.Manifest.Digest}}'

Run the image by that digest, as ghcr.io/perfloop/relay@<digest>. Each digest is signed with keyless cosign by the workflow that built it. Verify the signature before you run the image.

cosign verify \
  --certificate-identity https://github.com/perfloop/relay/.github/workflows/image.yml@refs/heads/main \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/perfloop/relay@<digest>

Each image carries an SPDX SBOM attestation. To compare the binary with your own build of the same commit, see docs/image.md.

2 · create the relay token

In Perfloop Setup, in Model inputs, find Relay tokens under Telemetry. Enter a token name of 1 to 120 characters that no other live key uses, and press Create relay token. Copy the token now; Perfloop shows it once. Hold it in your shell's environment, never in a file or on a command line that shell history records.

read -rs PERFLOOP_RELAY_TOKEN && export PERFLOOP_RELAY_TOKEN   # paste the token, then Enter
read -rs VM_READ_TOKEN && export VM_READ_TOKEN                  # the provider's read token, if it needs one

3 · write the configuration

Name each upstream with one lowercase label, its provider kind (the provider name in lowercase, as in the example), its URL inside your network, and the headers it needs. A value written as ${NAME} is read from the environment when the relay starts.

api: https://app.perfloop.ai
token: ${PERFLOOP_RELAY_TOKEN}
upstreams:
  - name: vm
    kind: victoriametrics
    url: http://vmselect.monitoring.svc:8481/select/0/prometheus
    headers:
      Authorization: Bearer ${VM_READ_TOKEN}
  - name: logs
    kind: loki
    url: http://loki-gateway.monitoring.svc
    headers:
      X-Scope-OrgID: <loki tenant>

A provider path prefix, such as a VictoriaMetrics tenant path, belongs in the upstream URL. For a multi-tenant Loki, set X-Scope-OrgID in the upstream headers, as above; Setup takes no tenant for a relay source. Leave the headers block out for an upstream that needs none. Every field is in docs/configuration.md.

Before you run the relay, print every request this file lets it forward. Placeholder values are enough for the secrets here.

docker run --rm -v "$PWD/relay.yaml:/etc/perfloop-relay/relay.yaml:ro" \
  -e PERFLOOP_RELAY_TOKEN=placeholder -e VM_READ_TOKEN=placeholder \
  ghcr.io/perfloop/relay@<digest> -print-routes

Go processes that expose net/http/pprof are an upstream of kind pprof. It has no URL; it lists targets, one lowercase label naming each process and the http(s) base its handler is served from, and Perfloop names one target per read. This upstream, on its own, is a complete configuration:

api: https://app.perfloop.ai
token: ${PERFLOOP_RELAY_TOKEN}
upstreams:
  - name: go
    kind: pprof
    targets:
      api-1: http://api-1.prod.svc:6060
      worker-1: http://worker-1.prod.svc:6060
    # Optional. The longest CPU profile a read may ask for, in seconds:
    # default 30, at most 45.
    max_seconds: 30
    # Optional. Profile reads in flight across these targets, in this relay
    # process: default 1, at most 8.
    max_concurrent: 1

The relay forwards only GET requests to /debug/pprof/profile, heap, allocs, goroutine, mutex, and block, with seconds as the only query key, and refuses a read over either cap before it reaches a process. Per read Perfloop retains the one profile the process returned (its pprof bytes, at most 32 MB), the target name, the profile type, the requested window, and the moment the last byte arrived; it never merges profiles across reads or processes, and a read that answers outside its window is refused and retains nothing.

4 · run it

Run the image with the configuration mounted at /etc/perfloop-relay/relay.yaml and each secret the file names passed from the variables you set in step 2. The image is built for linux/amd64. It needs outbound access to app.perfloop.ai on 443 and to your upstreams. Nothing needs to reach it.

docker run -d --name perfloop-relay \
  -v "$PWD/relay.yaml:/etc/perfloop-relay/relay.yaml:ro" \
  -e PERFLOOP_RELAY_TOKEN -e VM_READ_TOKEN \
  ghcr.io/perfloop/relay@<digest> \
  && unset PERFLOOP_RELAY_TOKEN VM_READ_TOKEN   # once the container holds them

On Kubernetes, start from examples/kubernetes/relay.yaml: a Deployment and the shape of its Secret, with a NetworkPolicy that allows egress to DNS, to the named upstream, and to public addresses on port 443, nothing else. To confine that last rule to Perfloop's host, use an egress gateway or a policy that understands host names.

The relay keeps two tunnels to Perfloop open and reopens one that ends. The log line tunnel open means it is connected. More than one relay can run for your account; Perfloop sends each read to any open tunnel and moves it to another only when the first fails before a response starts; an answer such as unknown upstream is final. So every connected relay must carry the same upstreams.

5 · connect the source in Setup

In Perfloop Setup, choose the provider and enter relay://vm as the endpoint, with the upstream name you chose. Leave Authorization empty, and for Loki leave Tenant empty too. Perfloop refuses both for a relay source. Register the label selectors as for any other connection. Perfloop validates each selector through the relay before it saves the connection.

For Go processes, choose Go pprof, enter relay://go as the endpoint, and register one target name per selector: api-1 and worker-1 here. Perfloop proves each target with one heap snapshot when it saves the connection, so one connection holds at most eight targets; register further processes as another connection to the same upstream.

Data that crosses

Every read starts at Perfloop. Its proxy checks the source, the registered label scope, the query, and the time window before the request goes down a tunnel; the relay cannot widen those checks. Inside a tunnel Perfloop is the client, so the relay can only answer and never starts a request into Perfloop.

  • Toward your network. The validated query and its time window, as a GET addressed to the upstream by the name you chose. Of the request headers Perfloop sends, the relay passes Accept, Accept-Encoding, User-Agent, and X-Scope-OrgID to your provider. Your configured headers are set last and replace any header of the same name. The relay never sends the configured URL to Perfloop; the stored endpoint is the upstream name alone.
  • Toward Perfloop. The provider's response for that read: its status and headers, and its body, as the provider sent them. The relay removes hop-by-hop headers and every Perfloop-* header, and it aborts a read whose body is larger than 32 MiB. The relay sends nothing on its own and keeps nothing.
  • Stored results. For Prometheus, VictoriaMetrics, and Loki, the same as for a direct connection to that provider: see its guide for the query limits and what Perfloop keeps. For a pprof target Perfloop retains, per read, the one profile the process returned (its pprof bytes, at most 32 MB), the target name, the profile type, the requested window, and the moment the last byte arrived; it never merges profiles across reads or processes, and a read that answers outside its window is refused and retains nothing.

The relay does not inspect response bodies or scan them for credentials, and of the response headers it removes only the hop-by-hop set and every Perfloop-* header. If a provider or a gateway in front of it writes a request header into a response body or another response header, that value reaches Perfloop. The providers the relay reads do not do this by default. Give each upstream a read-only credential.

The relay forwards only GET requests without a body to the documented read routes of the provider kind you configured. It refuses admin and write routes, and any route it does not know, before any request reaches your provider, and it logs one line per read. The route table and the code that enforces it are in docs/security.md.

While no relay of yours is connected, a read fails with the source reported unavailable. Perfloop does not answer it from a cache or with partial data.

Change or remove access

The relay reads its configuration once, when it starts. After you change the file or a secret it names, start every relay that uses it again with the new values and check each log for tunnel open. On Docker, remove the container and run it again; a restart keeps the old environment. On Kubernetes, scale the Deployment to zero and back; a ConfigMap or Secret change starts no rollout by itself, and a rolling update connects the old and the new configuration at the same time. To point a source at another upstream URL, change the URL and start the relays again; the endpoint in Setup keeps the name.

To stop all reads, stop every relay of your account, or revoke every relay token. While one relay is connected on any live token, Perfloop reads through it. To stop reads of one provider while the relay keeps running, revoke that provider's credential in your network, or remove its upstream from the configuration and start every relay that carries it again.

Revoke a relay token in Setup, in Relay tokens. Perfloop checks the token of each open tunnel every 30 seconds. At the check that finds a token revoked, its tunnels stop taking new reads and close within ten more seconds, when a read in flight has finished or that time is up. If Perfloop cannot read its database at a check, the tunnel stays open until a later check succeeds. A revoked token cannot open a new tunnel. To rotate a token, create the new one, start the relays again with it, then revoke the old one.

Removing access does not delete connection details or results already stored in Perfloop.

Security questions: security@perfloop.ai