SevyDevy

Guide · Backend

Oblivious HTTP Explained: How Relays and Gateways Hide Your IP

13 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Oblivious HTTP (OHTTP), defined in RFC 9458, an internet standards document, sends each request through two separate servers, so neither learns both who you are and what you asked. The relay sees your IP address, the number your connection comes from, but not the request. The gateway sees the request but not your IP.
  • Your app seals each request with HPKE (Hybrid Public Key Encryption), a published recipe that encrypts a message so only one private key can open it. Here that key belongs to the gateway.
  • The split protects users only if two different organisations, called operators, run the relay and the gateway. It hides who sent a request, not what it says: a cookie or name inside still identifies the user.
  • As of 19 October 2026, Cloudflare's self-serve OHTTP Gateway, a hosted gateway, has been in closed beta since 2 October. It refuses requests sent from Cloudflare Workers, its serverless code platform, or from sites whose traffic runs through Cloudflare.
  • Beginners can stop after the section on when Oblivious HTTP is the wrong tool. Engineers then get the byte layout, a two-library test measuring 55 bytes of overhead per request with the RFC's example algorithms, padding, and the replay and key-tagging attacks.

What goes wrong when one server sees both your IP and your question?

It can link the two, so a private question gets a return address. An IP address plus a timestamp can narrow a request down to one home network or one mobile connection.

This happens without anyone planning it. nginx, a popular web server, writes the client IP address and the local time into its access log by default.

Picture a common setup. A health app lets people type a symptom without logging in and get advice, about 2 lakh (200,000) searches a day. The setup is made up, but the problem is real.

One night someone searches for "chest pain after climbing stairs". The next morning that search sits in the server log, right beside the user's IP address and the exact time. Nobody meant to collect it.

The team wants to answer every search without learning who sent it. Oblivious HTTP, a standard from the IETF (the Internet Engineering Task Force, which writes internet protocols), is built for exactly this. The rest of this guide follows that one app.

Which words do you need before learning Oblivious HTTP?

You need eight words. RFC 9458 uses longer names for the four parties, shown in brackets.

  • Client: the app that seals and sends the request, here on the user's phone.
  • Relay (Oblivious Relay Resource): a middle server that passes sealed requests along. It sees who connects but cannot read what they send.
  • Gateway (Oblivious Gateway Resource): the server that opens a sealed request, sends it on as normal HTTP and seals the answer.
  • Target (Target Resource): the real API that answers. It may not even know Oblivious HTTP is in use.
  • Operator: the organisation that runs a server and can read its logs.
  • HPKE (Hybrid Public Key Encryption, RFC 9180): a published recipe for encrypting a message that only one private key can open.
  • Key configuration: the gateway's public record of a key ID, its algorithms and its public key.
  • Binary HTTP (RFC 9292): a compact byte format for a whole HTTP request or response.

What is Oblivious HTTP and how does it work?

Oblivious HTTP is a standard way to send an HTTP request through two separate servers. The relay knows the sender, and the gateway knows the question. Neither one learns both.

Think of a sealed letter sent by courier. The courier knows your address but cannot open the envelope. The reader opens the letter, but it arrives from the courier's depot, not your house.

Map the parts: the courier is the relay, the envelope is HPKE encryption, and the reader is the gateway. The depot's return address is the relay's IP address, the only one the gateway sees.

Where the picture breaks: a courier could steam open an envelope, but HPKE ciphertext needs the gateway's private key. That holds as long as the app has the gateway's real public key. And unlike a letter, the answer travels back sealed through the same courier.

The diagram follows one search, one box per message.

One symptom search through Oblivious HTTP

Notice that the relay only ever carries sealed bytes, and the gateway only ever sees the relay's address, never the phone's.

Diagram connections:

  • Key configuration to Sealed search (App seals with the key)
  • Sealed search to Forwarded sealed search
  • Forwarded sealed search to Plain search request (Gateway decrypts)
  • Plain search request to Plain answer
  • Plain answer to Sealed answer (Gateway encrypts)
  • Sealed answer to Answer on the phone
  • Key configuration: Gateway → App, fetched ahead of time
  • Sealed search: App → Relay, POST of type message/ohttp-req
  • Forwarded sealed search: Relay → Gateway, the user's IP left behind
  • Plain search request: Gateway → Symptom API, ordinary HTTP
  • Plain answer: Symptom API → Gateway, see a doctor soon
  • Sealed answer: Gateway → Relay, type message/ohttp-res
  • Answer on the phone: Relay → App, still sealed to the relay

RFC 9458 says a relay must not add information that might identify the client when it forwards a request. It names the Via and Forwarded headers, which record earlier hops, as the kind of thing generic proxies add. So the gateway sees a request from the relay, not from the user's phone.

What changed when Cloudflare opened its OHTTP Gateway beta in October 2026?

Cloudflare opened a closed beta of a self-serve OHTTP Gateway on 2 October 2026. Before that, its OHTTP product covered only the relay side.

That 2022 relay product, Privacy Gateway, is now renamed Cloudflare OHTTP Relay. The new gateway will be sold as a paid add-on to a customer's domain. As of 19 October 2026, no price is published.

Clients send sealed requests to the fixed path /.well-known/ohttp-gateway on the customer's domain. A GET request to that path returns the public keys, which Cloudflare manages. The gateway decrypts each request and makes an ordinary request to the customer's own server.

The detail that drew attention is a refusal. The gateway will not decrypt requests sent from Cloudflare Workers, its serverless code platform, or from websites that route their traffic through Cloudflare. Cloudflare says this ensures it never sees both who sent a request and what it says. InfoQ reported the refusal on 13 October 2026.

Two things the announcement does not say, as of 19 October 2026: which HPKE algorithms the gateway uses, and how often its keys rotate. Read both from the key configuration your gateway serves, and recheck after the beta.

What do the relay and the gateway each see?

The relay sees the client's IP address and the time and size of each sealed message. The gateway sees the full request but only the relay's address.

Who sees what in one symptom search
PartySees the user's IP address?Sees the search text?Who runs it in the example
Client appYesYesThe user's phone
RelayYesNo, only sealed bytes, their sizes and timingA separate relay company
GatewayNo, only the relay's addressYes, after decryptingThe health app's team
Target APINoYesThe health app's team
Wi-Fi or mobile networkYesNo, the link to the relay uses HTTPSThe user's internet provider

In the example, the health app runs both the gateway and the target. RFC 9458 allows that and says it does not affect client privacy. Only the relay and the gateway must have different operators.

Timing and size still leak a little. RFC 9458 warns that a malicious relay can study message sizes and timing, called traffic analysis. It suggests padding, meaning filler bytes, to make that harder.

Pause and predict: the relay holds half the story and the gateway holds the other half. What happens if one company runs both?

Why must the relay and the gateway be run by separate operators?

Because one operator running both could join the two logs by time and put a user's IP next to their search. RFC 9458 says the relay cannot be operated by the same entity as the gateway.

Back to the courier: if the courier and the reader are one company, the envelope protects nothing.

Why the two logs must stay with two operators

Each log alone links nothing. Notice that the only step that joins IP to question is one operator holding both logs.

Diagram connections:

  • Relay log to One operator for both?
  • Gateway log to One operator for both?
  • One operator for both? to Logs joined by time (Yes)
  • One operator for both? to Logs stay apart (No)
  • Relay log: User's IP, 9:14 pm, 1.2 KB sealed (made up)
  • Gateway log: Chest pain search, 9:14 pm, via the relay
  • One operator for both?: The question that decides the privacy
  • Logs joined by time: IP and search linked: privacy is gone
  • Logs stay apart: No single party sees both who and what

This is why Cloudflare's gateway refuses traffic from its own Workers and proxied hosts. Its post says the relay and app server must be run by separate, non-colluding parties, and suggests a third-party relay.

The gateway can also check that requests come from the agreed relay. Cloudflare Access, its product that checks every caller's identity before letting it in, runs before decryption, with options such as mutual TLS, where both sides show certificates.

When is Oblivious HTTP the wrong tool?

It is the wrong tool when the request itself identifies the user, or when requests must be linked, as with a login. RFC 9458 says plainly that it has limited applicability.

  • Logged-in features. RFC 9458 calls cookies the most obvious feature that can link requests, and login credentials do the same.
  • Identifiers in the body. Cloudflare warns that it is up to you not to send things like an email address or username inside the request.
  • General browsing. Each service needs its own gateway and an agreed relay, so it suits a few chosen APIs rather than whole websites.
  • Tiny user bases. With only a handful of users behind a relay, each one has little cover. RFC 9458 likewise warns against small groups of clients sharing one setup.

The symptom app fits well, since its searches need no login and are independent. RFC 9458 lists similar uses: DNS queries, telemetry, anonymous surveys, search queries and map tiles.

Beginners can stop here: you now know the four parties, what each one sees, why two operators matter and when to skip Oblivious HTTP. The sections below are for engineers.

How does HPKE encryption seal an Oblivious HTTP request?

The app encrypts the request to the gateway's public key using HPKE. It puts a 7-byte header in front saying which key and algorithms it used. Only the gateway's private key can open it.

The key configuration comes first

Each key configuration has a 1-byte key ID, a 2-byte KEM ID, the public key and the algorithm pairs the gateway accepts. A KEM, or key encapsulation mechanism, creates a fresh shared secret that only the private key holder can recover.

RFC 9458 does not define how the app gets this record. Cloudflare suggests fetching keys over a different IP address from the one used for requests, for stronger privacy.

Sealing the request, step by step

  1. Pack the request

    The app turns the search into a Binary HTTP message.

  2. Build the header

    It writes 7 bytes: the key ID, then the KEM, KDF (key derivation) and AEAD (encryption) algorithm IDs.

  3. Build the info string

    It joins the text "message/bhttp request", one zero byte and the header, tying the encryption to this purpose and key.

  4. Set up HPKE

    HPKE setup in base mode needs only the gateway's public key. It gives an encapsulated key, called enc, and a sending context.

  5. Seal and send

    It encrypts the Binary HTTP bytes into ciphertext, then sends header, enc and ciphertext to the relay as one POST body.

The Python below builds the header and info string for the example key, using algorithm IDs from RFC 9180. Real sealing then needs an HPKE library.

python
import struct

# The gateway's key configuration (key ID 1, as in RFC 9458 Appendix A)
KEY_ID = 1
KEM_X25519 = 0x0020    # DHKEM(X25519, HKDF-SHA256)
KDF_SHA256 = 0x0001    # HKDF-SHA256
AEAD_AES128 = 0x0001   # AES-128-GCM

def request_header(key_id, kem_id, kdf_id, aead_id):
    # one 8-bit number, then three 16-bit numbers, big-endian
    return struct.pack("!BHHH", key_id, kem_id, kdf_id, aead_id)

hdr = request_header(KEY_ID, KEM_X25519, KDF_SHA256, AEAD_AES128)
info = b"message/bhttp request" + b"\x00" + hdr

print(hdr.hex())   # 01002000010001
print(len(info))   # 21 label bytes + 1 zero byte + 7 header bytes = 29

What just happened? In struct.pack, the format "!BHHH" means big-endian order: one 1-byte number, then three 2-byte numbers. Run it with Python 3 and you should see 01002000010001 and 29. A different hex string means the byte order is wrong.

A gateway rejects a key ID or algorithm it does not know. RFC 9458 says it can answer an unknown key ID with HTTP 422 (Unprocessable Content). The app cannot rely on a clear error, because that error reply cannot be encrypted and may not reach it.

Sealing the answer

For the reply, the gateway exports a secret from the same HPKE session, using the label "message/bhttp response". It adds a random nonce, a number used once (16 bytes with AES-128-GCM), and derives a fresh key from that secret, the request's enc and the nonce. The relay cannot open the answer. The app can, and so can anyone who later steals the gateway's private key.

How much bigger does Oblivious HTTP make each request?

With X25519, HKDF-SHA256 and AES-128-GCM, the algorithms in RFC 9458's own example, a sealed request is 55 bytes bigger than the Binary HTTP message inside it. A sealed response is 32 bytes bigger.

Bytes added by Oblivious HTTP with X25519, HKDF-SHA256 and AES-128-GCM (measured 19 October 2026)
PartRequest bytesResponse bytesWhat it is
Header70Key ID and three algorithm IDs
enc320The X25519 encapsulated key
Response nonce016Random bytes, the larger of AES-128-GCM's key and nonce sizes
AEAD tag1616The check value that detects tampering
Total added5532Before any padding

We checked these numbers across two separate implementations on 19 October 2026. A Python gateway built on pyhpke 0.6.5 opened a request sealed by ohttp-ts 0.6.1, a TypeScript OHTTP library. ohttp-ts then opened the Python gateway's sealed reply.

A 44-byte payload became a 99-byte sealed request, and a 17-byte answer became a 49-byte sealed response. Other algorithms change these numbers, so recompute them for your key configuration.

Padding changes the real size

ohttp-ts 0.6.1 pads by default. Its Request helper pads the Binary HTTP message to at least 1,024 bytes, so short searches all look the same size to the relay.

In our run, a 5-character and a 32-character search both became 1,079-byte sealed requests. A 2,000-character body became 2,231 bytes. With padding switched off, the 5-character search was 138 bytes.

So padding costs about 1 KB per short request and hides its length. The time cost is one extra network hop plus the encryption work at each end. Measure that on your own traffic.

How can attackers track users or replay Oblivious HTTP requests?

A gateway can track users by handing each one a different key, and an attacker can resend an old sealed request. HPKE alone stops neither.

Unique keys can tag users

If the gateway gave one user their own key ID, every request with that ID would point to them. RFC 9458 calls preventing unique client configurations critical, because each active configuration splits users into smaller groups.

The check is easy to state: every key configuration in use must be shared by many users, never handed to one user.

Replays and the HTTP Date header

Someone who copies a sealed request can send it again. In our test, both pyhpke 0.6.5 and ohttp-ts 0.6.1 opened the same sealed request twice without complaint. RFC 9458 makes the gateway and the application responsible: reject replays, or make sure a replay does no harm.

RFC 9458 says clients should put a Date header inside the sealed request. The gateway can reject a request whose date falls outside its time window, or one it has already answered in that window.

The gateway can answer with a 400-series error that carries its own date. The client may then retry once, with a corrected date and a fresh HPKE context.

A leaked key opens old requests

RFC 9458 gives no forward secrecy for requests or answers during a key configuration's lifetime. Forward secrecy would keep old messages safe if a key leaked later. In our test, the gateway's private key alone opened a request captured earlier, and the answer to it.

RFC 9458 suggests rotating keys and deleting old ones as a partial fix. Give each new key to all users, not a few at a time, so the rotation does not create small groups.

What is chunked Oblivious HTTP, and is it a standard yet?

Chunked Oblivious HTTP is a proposed variant that seals a message in pieces, and it is not a standard yet. As of 19 October 2026 it is draft-ietf-ohai-chunked-ohttp-08, dated 18 February 2026.

Plain Oblivious HTTP seals each message as one block, so the receiver must get all of it before it can check and open it. The chunked version encrypts each piece on its own and marks the final piece.

The draft sits in the RFC Editor queue, marked as waiting for a reference that is not yet published. Cloudflare says its new gateway supports chunked OHTTP, and ohttp-ts 0.6.1 implements version 08. Treat it as coming, not settled.

How do you explain Oblivious HTTP in a system design interview?

Start with the threat, draw four boxes, then say what each box can and cannot see.

  1. Name the threat

    One server seeing both the IP address and the request can link a person to a sensitive question.

  2. Draw the four parties

    Client, relay, gateway and target. The relay forwards sealed bytes, and the gateway decrypts them.

  3. Explain the sealing

    The client encrypts with HPKE to the gateway's public key from a key configuration shared by many users.

  4. Insist on separate operators

    Different, non-colluding organisations run the relay and gateway, and the gateway can authenticate the relay.

  5. Cover the attacks

    Per-user keys as tracking tags, replays limited with a Date window and a duplicate check, and size and timing analysis at the relay.

  6. State the costs and limits

    One extra hop, 55 bytes per request before padding, no logins or identifiers inside, and independent requests only.

Rough numbers help. 2 lakh searches a day is about 2.3 requests a second on average, each 55 bytes bigger before padding. The traffic is made up; the overhead is measured.

Questions people ask

Is Oblivious HTTP the same as a VPN?
No. A VPN provider sees your IP address and every site you connect to, so you trust one company. Oblivious HTTP splits that knowledge between two operators, and it works per request for chosen APIs, not for all your traffic.
Does Oblivious HTTP hide my data from the server that answers?
No. The gateway decrypts the request and the target reads it in full. Oblivious HTTP only hides who sent it. If the request contains a name, phone number or login cookie, the server can still identify you.
Can one company run both the relay and the gateway?
Not if you want the privacy promise to hold. RFC 9458 says the relay cannot be operated by the same entity as the gateway. One operator could join both logs by time and link each IP address to each request.
Do I need Cloudflare to use Oblivious HTTP?
No. RFC 9458 is an open IETF standard, and any relay and gateway that follow it can work together. You can run a gateway yourself with an open-source library and contract a relay from a separate company.
Does Oblivious HTTP make requests slower or bigger?
Yes, a little. Each request takes one extra network hop, plus encryption work at the app and the gateway. With X25519 and AES-128-GCM, a request grows by 55 bytes and a response by 32, before any padding. Measure the time cost on your own traffic.
Is chunked Oblivious HTTP a standard yet?
Not yet. As of 19 October 2026 it is an Internet-Draft, version 08 from 18 February 2026, waiting in the RFC Editor queue. Some gateways and libraries already support it, but its details could still change.

Sources and further reading

Practise this with Lala

Practise