---
title: "Build Your Own HTTP/3 Server"
author: "Rantideb Howlader"
date: "2026-02-10T00:00:00.000Z"
canonical_url: "https://www.ranti.dev/blog/http3-quic-server"
license: "CC-BY-4.0"
---


Welcome to the cutting edge of the web.

If you have already followed the guide on building an HTTP/1.1 server, you know how to talk to the kernel using sockets. You know how to handle TCP connections. But today, we are going to break everything you learned and start over.

Modern web performance is no longer about just sending bytes faster. It is about a fundamental problem in how the internet was designed forty years ago. We are going to learn why TCP is holding us back and how a new protocol called QUIC is fixing it.

By the end of this guide, you will have a working HTTP/3 server written in Python.

## The Problem with TCP

Before we write a single line of code, we need to understand why we are doing this. If TCP worked fine for decades, why are we changing it now?

### 1. Head-Of-Line Blocking

In our previous server, we used TCP. TCP is a reliable stream. If you send three images over a single TCP connection, they must arrive in order.

Imagine three trucks driving down a one-lane road:

1. Truck A (Small CSS file)
2. Truck B (Giant Image)
3. Truck C (JavaScript file)

If Truck B gets a flat tire, Truck C cannot pass it. Even though Truck C is perfectly fine, it must sit and wait until Truck B is fixed. In networking terms, if one packet of the giant image is lost, your whole website stops loading while the computer waits for that one tiny piece to be resent.

This is called **Head-of-Line Blocking** (HOL Blocking). It is the single biggest performance killer on the mobile web.

### 2. The OSI Layer Trap

In our HTTP/1.1 server, we did not worry about security. In the real world, you use TLS (HTTPS).

TCP and TLS live in different layers.

1. The computer does a TCP handshake (Takes time).
2. Then it does a TLS handshake (Takes more time).

This means before a single byte of your webpage is sent, the browser and server have already chatted back and forth several times just to say hello securely. On a slow 4G connection, this adds seconds of delay.

### 3. The UDP Solution

To fix this, Google (and later the IETF) decided to move away from TCP. Instead, we use **UDP**.

UDP is lean. It does not care about order. It does not care about reliability. It just throws packets at the destination and hopes for the best.

Wait, how can a website work if the packets are unreliable?

The answer is **QUIC**. We take the speed of UDP and we build the reliability and security _inside_ the protocol itself. Instead of having a TCP layer and a TLS layer, we mash them together into one fast layer.

## Under the Hood of QUIC

QUIC is the heart of HTTP/3. You cannot understand the server without understanding the protocol.

### 4. Direct TLS Integration

In HTTP/3, the handshake is combined. Since we are using UDP, we do not need to wait for a connection to be established before starting the security talk.

In many cases, if you have visited the site before, you can use **0-RTT** (Zero Round Trip Time). This means the browser sends the request _inside the first packet_. The server receives the packet, decodes the request, and sends the data back immediately.

There is no waiting. It is as close to instant as physics allows.

### 5. Connection IDs vs IP Addresses

Have you ever been on a Zoom call on your phone, walked out of your house, and had the call drop as you switched from Wi-Fi to 5G?

This happens because TCP identifies a connection by your IP address. When your IP changes from Wi-Fi to 5G, the old TCP connection is dead.

QUIC uses a **Connection ID**. Think of it like a name tag. Even if your address changes, your name tag stays the same. The server sees the same name tag and keeps the data flowing. You do not even notice the switch. This is called **Connection Migration**.

### 6. Independent Streams

Remember the trucks on the one-lane road?

QUIC turns that one-lane road into a multi-lane highway. Each file (CSS, JS, Image) gets its own **Stream**. If the image stream loses a packet, the CSS and JS streams keep moving in their own lanes.

One failure no longer breaks the whole page.

#### Frames and Packets

In TCP, we just sent a pile of bytes. In QUIC, we send **Packets**, and inside those packets are **Frames**.

Common Frames:

- **STREAM Frame**: Contains your actual webpage data.
- **ACK Frame**: Tells the other side 'I got your packet'.
- **CRYPTO Frame**: Handles the security handshake.
- **PADDING Frame**: Makes packets a certain size to prevent hackers from guessing what is inside.

#### QPACK Header Compression

In HTTP/2, we used HPACK to compress headers. But HPACK required strictly ordered streams. Since QUIC is unordered, we needed a new way.

**QPACK** allows us to compress headers even when packets arrive out of order. It uses a dynamic table to remember common headers (like `User-Agent`) so we do not have to send them repeatedly.

## Preparing Your Environment

We cannot build a QUIC server with just a simple Python script using standard libraries. The standard library is too old for this.

### 7. Why We Use aioquic

To build this, we will use a library called `aioquic`.

QUIC is incredibly complex to write from scratch. It handles encryption, congestion control, and stream management. Coding this yourself would take months. `aioquic` is a high-performance implementation that lets us focus on the HTTP/3 logic.

#### Installing Dependencies

First, make sure you have Python 3.9 or higher.

Create a new folder and a virtual environment:

```bash
mkdir http3-server
cd http3-server
python3 -m venv venv
source venv/bin/activate
```

Now install the library:

```bash
pip install aioquic cryptography
```

#### The Role of Cryptography

QUIC mandates TLS 1.3. You cannot have unencrypted HTTP/3.

The `cryptography` library will help us generate certificates. In production, you would use Let's Encrypt, but for our 'Step 1' we will make our own.

### 8. Generating Self-Signed Certificates

We need a certificate and a private key. This is the 'ID Card' our server shows the browser.

#### Certification Authority (CA) Theory

Browsers only trust certificates signed by someone they know (like Digicert or Google). Since we are making our own, your browser will show a warning. That is fine for development.

#### Generating the Files

Save this script as `generate_cert.py` and run it:

```python
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import datetime

# Generate a private key
key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
)

# Details about our server
subject = issuer = x509.Name([
    x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
    x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'California'),
    x509.NameAttribute(NameOID.LOCALITY_NAME, u'San Francisco'),
    x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'My HTTP3 Server'),
    x509.NameAttribute(NameOID.COMMON_NAME, u'localhost'),
])

# Create the certificate
cert = x509.CertificateBuilder().subject_name(
    subject
).issuer_name(
    issuer
).public_key(
    key.public_key()
).serial_number(
    x509.random_serial_number()
).not_valid_before(
    datetime.datetime.utcnow()
).not_valid_after(
    # Our certificate lasts for one year
    datetime.datetime.utcnow() + datetime.timedelta(days=365)
).add_extension(
    x509.SubjectAlternativeName([x509.DNSName(u'localhost')]),
    critical=False,
).sign(key, hashes.SHA256())

# Save the private key
with open('key.pem', 'wb') as f:
    f.write(key.private_key_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption(),
    ))

# Save the certificate
with open('cert.pem', 'wb') as f:
    f.write(cert.public_bytes(serialization.Encoding.PEM))

print('[+] Certificate and Key generated!')
```

Run it: `python generate_cert.py`.

## Building the Server

Now for the main event. We are going to build a server that handles the QUIC handshake and answers an HTTP/3 request.

### 9. The HTTP/3 Frame Logic

In HTTP/1.1, we sent plain text. In HTTP/3, we send encoded frames.

When a browser connects, it sends a **HEADERS frame**. This frame contains the HPACK/QPACK encoded headers (like the path `/`). Then it might send a **DATA frame** if it is a POST request.

Our server must:

1. Receive the QUIC connection.
2. Initialize an HTTP/3 state machine for that connection.
3. Decipher the HEADERS frame.
4. Send its own HEADERS frame (200 OK) and its own DATA frame (Hello World).

#### Setting Up the Base Server

Create a file named `http3_server.py`. We will start by setting up the logger and the configuration.

```python
import asyncio
import os
from aioquic.asyncio import QuicConnectionProtocol, serve
from aioquic.h3.connection import H3Connection
from aioquic.h3.events import HeadersReceived, DataReceived
from aioquic.quic.configuration import QuicConfiguration

# Configuration for our QUIC server
configuration = QuicConfiguration(
    is_client=False,
    # Standard ports for certs we just made
    certificate_path='cert.pem',
    private_key_path='key.pem',
)
```

#### Handling the Events

Everything in `aioquic` is event-driven. We do not manually read from a socket like we did in the old server. Instead, the library tells us: 'Hey, I received some headers!' or 'Hey, the connection closed!'.

### 10. The Request Handler

We need a class that manages the life of a single connection.

Think of this class as a 'Waiter'.

1. The Waiter greets the customer (Handshake).
2. The Waiter takes the order (HeadersReceived).
3. The Waiter brings the food (Send Headers + Data).
4. The Waiter clears the table (ConnectionClosed).

```python
class Http3Handler(QuicConnectionProtocol):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Each connection gets its own H3 instance
        self._h3 = H3Connection(self._quic)

    def quic_event_received(self, event):
        # A standard QUIC event (like packet received)
        # We pass it to the H3 state machine
        for h3_event in self._h3.handle_event(event):
            if isinstance(h3_event, HeadersReceived):
                self._handle_request(h3_event.stream_id, h3_event.headers)

    def _handle_request(self, stream_id, headers):
        # Extract the path from headers
        # Headers are a list of tuples: [(b':method', b'GET'), (b':path', b'/')...]
        headers_dict = {k: v for k, v in headers}
        path = headers_dict.get(b':path', b'/').decode()

        print(f'[+] Client requested: {path}')

        # Prepare the response body
        response_body = f'Welcome to HTTP/3! You asked for {path}'.encode()

        # 1. Send Response Headers (Status 200 OK)
        self._h3.send_headers(
            stream_id=stream_id,
            headers=[
                (b':status', b'200'),
                (b'server', b'python-aioquic-example'),
                (b'content-type', b'text/plain'),
                (b'content-length', str(len(response_body)).encode()),
            ],
        )

        # 2. Send Response DATA
        self._h3.send_data(stream_id=stream_id, data=response_body, end_stream=True)

        # 3. Flush packets to the network
        self.transmit()
```

#### Running the Main Loop

Finally, we need to tell Python to listen on a specific UDP port (usually `4433` for testing).

```python
async def main():
    print('[*] Starting HTTP/3 Server on udp://localhost:4433')
    await serve(
        '127.0.0.1',
        4433,
        configuration=configuration,
        create_protocol=Http3Handler,
    )
    # This keeps the server running forever
    await asyncio.Future()

if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass
```

### 11. Understanding AsyncIO

Unlike our old server which used `threading`, this server uses `asyncio`.

In the threading model, if you have 1000 users, you need 1000 threads. Threads are heavy. They each take memory.

In the async model, there is only one thread. It works like a kitchen with one chef. The chef puts the pasta in the water, and while it boils, he chops the onions. He does not sit and stare at the pasta.

In our server, when we wait for a UDP packet, the CPU is free to do other things. This allows a single script to handle thousands of users without crashing.

#### The Transmit function

The `self.transmit()` call is vital. In TCP, the kernel handles when to send data. In QUIC, the application (us) has more control. `transmit()` tells the library: 'Okay, I have queued up all my responses, now turn them into UDP packets and send them out!'.

### 12. Handling Streams and IDs

QUIC uses IDs to keep track of conversations.

- **Even numbers** (0, 2, 4...) are for things the client starts.
- **Odd numbers** (1, 3, 5...) are for things the server starts (like Server Push, but that is advanced).

When a browser opens your site, it starts Stream 0 for `index.html`. If it needs `styles.css`, it starts Stream 4.

Our code inside `_handle_request` uses the `stream_id` provided by the client. This ensures our response goes back to the correct file request! If we messed this up, the browser would get the CSS file data but think it was the HTML.

## Verification and Advanced Tuning

You have a server. Now you need to prove it works.

### 13. Testing with a QUIC Client

You cannot use a standard browser by just typing the URL yet. Why? Because browsers do not know your server supports HTTP/3. They usually try HTTP/1.1 first, then see an 'Alt-Svc' header (Alternative Service) that tells them: 'Hey, I have a faster server on port 4433!'.

To test directly, we need a specialized client.

#### Using curl with HTTP/3

Most versions of `curl` do not have HTTP/3 built-in because it requires special TLS libraries (like BoringSSL or QuicTLS).

If you have a modern Mac with Homebrew, you might be able to install a version that supports it. But a better way is to use the experimental client provided by `aioquic`.

```bash
# Run this in a second terminal window
python -m aioquic.examples.http3_client --ca-certs cert.pem https://localhost:4433/
```

This client is 'smart'. It knows how to use the `cert.pem` we made so it does not complain about security.

### 14. The Alt-Svc Header

If you want a real browser (like Chrome) to use your server, you need to tell it about it.

When a browser visits your site over HTTP/1.1, you send this header:
`Alt-Svc: h3=':4433'; ma=86400`

This tells the browser: 'For the next 24 hours (86400 seconds), you can find an HTTP/3 version of this site on port 4433'.

The next time the user types your URL, the browser will skip the TCP handshake entirely and go straight to the QUIC port.

### 15. Handling Connection Migration (The Wi-Fi Switch)

This is the 'magic' part of QUIC.

In our `Http3Handler`, the library is doing a lot behind the scenes. When a packet arrives from a _new_ IP address but has the _same_ Connection ID, the library updates the connection state automatically.

Your code in `_handle_request` does not even need to change. It just keeps processing data. This is why HTTP/3 feels so robust on mobile phones.

#### Probing

Wait, how does the server know the new IP address isn't a hacker?

QUIC uses **Path Validation**. The server sends a random number (a CHALLENGE) to the new address. If the client can repeat that number back, the server knows: 'Okay, the person who had the Connection ID really is at this new address'.

### 16. Server Push in HTTP/3

In the old days, if a browser wanted a page, it asked for `index.html`. It received it, read it, and saw it needed `main.css`. So it sent a second request.

With **Server Push**, the server says: 'I know you are going to ask for main.css anyway, so here it is!'.

In our `_handle_request` function, we could start a new stream (an odd-numbered one) and send the CSS data before the client even asks for it.

_Note: Browsers are starting to disable Server Push because it is hard to get right, but it is a powerful feature of the protocol._

### 17. Congestion Control in User Space

TCP congestion control is handled by your Windows or Mac kernel. If it is slow, you cannot fix it.

QUIC congestion control is handled by the application. This means if researchers find a faster way to handle congestion, they can just update the server software. You do not have to wait for a Microsoft or Apple update to get a faster internet.

`aioquic` implements several algorithms (like New Reno). It watches how many packets are getting lost and slows down or speeds up automatically to match your internet speed.

### 18. Scaling your Server

Our current script is just a toy. For a real production server, you would need:

1.  **Multiple Workers**: Use the `multiprocessing` module to run one server instance per CPU core.
2.  **Certificate Management**: Integrating with `acme` for automatic renewals.
3.  **Static File Handling**: Instead of just sending strings, you would use `aiofiles` to read from the disk without blocking the async loop.

### 19. The Future: HTTP/3 is here

Almost 30 percent of the internet is already using HTTP/3. YouTube, Facebook, and Google are leading the charge.

By building this server, you have moved past 'Web Development' and into 'Protocol Engineering'. You now understand how the packets move, how security is negotiated, and how to beat the limitations of the physical wires.

#### Final Checklist

- [x] Understand HOL Blocking.
- [x] Generate TLS certificates.
- [x] Set up an async loop.
- [x] Handle HeadersReceived events.
- [x] Send H3 Data frames.
- [x] Test with a QUIC client.

You are now a master of the modern web stack. Keep experimenting, keep building, and never stop digging into the layers.

---

#### Appendix: Full Server Source Code

Below is the complete, combined script for your reference.

```python
import asyncio
from aioquic.asyncio import QuicConnectionProtocol, serve
from aioquic.h3.connection import H3Connection
from aioquic.h3.events import HeadersReceived
from aioquic.quic.configuration import QuicConfiguration

class Http3Handler(QuicConnectionProtocol):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._h3 = H3Connection(self._quic)

    def quic_event_received(self, event):
        for h3_event in self._h3.handle_event(event):
            if isinstance(h3_event, HeadersReceived):
                self._handle_request(h3_event.stream_id, h3_event.headers)

    def _handle_request(self, stream_id, headers):
        headers_dict = {k: v for k, v in headers}
        path = headers_dict.get(b':path', b'/').decode()

        body = f'Hello from the QUIC world! Path: {path}'.encode()

        self._h3.send_headers(
            stream_id=stream_id,
            headers=[
                (b':status', b'200'),
                (b'content-type', b'text/plain'),
                (b'content-length', str(len(body)).encode()),
            ],
        )
        self._h3.send_data(stream_id=stream_id, data=body, end_stream=True)
        self.transmit()

async def main():
    configuration = QuicConfiguration(
        is_client=False,
        certificate_path='cert.pem',
        private_key_path='key.pem',
    )
    await serve('127.0.0.1', 4433, configuration=configuration, create_protocol=Http3Handler)
    await asyncio.Future()

if __name__ == '__main__':
    asyncio.run(main())
```


---

<!-- METADATA_START -->
## Metadata & Citations

### Further Reading
- [Build Your Own HTTP Server](https://www.ranti.dev/blog/build-your-own-http-server.md)
- [EKS Networking Deep Dive: Why Your Pods Can't Talk](https://www.ranti.dev/blog/eks-networking-vpc-cni.md)
- [Logging Off For A While](https://www.ranti.dev/blog/logging-off.md)

### Navigation
- [Back to Bio Hub](https://www.ranti.dev/.md)
- [Full Site Manifest](https://www.ranti.dev/llms.txt)

```json
{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "headline": "Build Your Own HTTP/3 Server",
  "author": {
    "@type": "Person",
    "name": "Rantideb Howlader"
  },
  "datePublished": "2026-02-10T00:00:00.000Z",
  "url": "https://www.ranti.dev/blog/http3-quic-server",
  "license": "https://creativecommons.org/licenses/by/4.0/",
  "isAccessibleForFree": true
}
```

### BibTeX
```bibtex
@article{http3-quic-server_2026,
  author = {Rantideb Howlader},
  title = {Build Your Own HTTP/3 Server},
  journal = {Rantideb Howlader Portfolio},
  year = {2026},
  url = {https://www.ranti.dev/blog/http3-quic-server},
  note = {Accessed: 2026-05-31}
}
```

### IEEE
Rantideb Howlader, "Build Your Own HTTP/3 Server," Rantideb Howlader Portfolio, 2026. [Online]. Available: https://www.ranti.dev/blog/http3-quic-server. [Accessed: 2026-05-31].

### APA
Rantideb Howlader. (2026). Build Your Own HTTP/3 Server. Rantideb Howlader. Retrieved from https://www.ranti.dev/blog/http3-quic-server

--- 
*This content is provided in research-grade Markdown format. Required Attribution: Cite as Rantideb Howlader (2026).*
<!-- METADATA_END -->