cloudflare / cloudflare/cloudflared

🐛 SSE over GET is not streamed in real-time on Quick Tunnel — all data is flushed only after the server closes the connection

Open
#1,449 3 comments 1 reaction 0 assignees View on GitHub
Priority: Normal Type: Bug
Dominant language
Go
Stars
15.6k
Forks
1.4k
PR merge metrics
No merged PRs in 30d

Description

### **Describe the bug**
When using FastAPI (`uvicorn` + `sse-starlette`) behind a **Quick Tunnel** (`cloudflared tunnel --url`), Server-Sent Events (SSE) behave very differently depending on whether the client uses `GET` or `POST`.

- When using `POST`, events are streamed **correctly in real-time** (e.g., one event every 0.5 seconds).
- When using `GET`, **none of the events are delivered to the client until the server finishes sending all events and closes the connection**. At that point, the entire stream is flushed to the client all at once.

This makes it impossible to use standard-compliant SSE (`GET` requests with streaming) for real-time applications like LLM token streaming or live dashboards.

If this behavior is intentional (e.g., due to buffering at the tunnel or edge), it should at least be **clearly documented**, because the delayed flush breaks the semantics of streaming responses over HTTP and silently disrupts many real-world use cases.

---

### **To Reproduce**

1. Save the following as `sse.py`:
```python
import asyncio
import json

import uvicorn
from fastapi import FastAPI, Request
from sse_starlette.sse import EventSourceResponse, ServerSentEvent

app = FastAPI()

async def generator(request: Request):
"""Generates data with a counter that increments every 0.5 seconds and terminates after 10 seconds."""

counter = 0
max_count = 20 # 20 iterations * 0.5 seconds = 10 seconds

while counter < max_count:
if await request.is_disconnected():
print('Client disconnected.')
break

# Generate data with counter
counter += 1
yield ServerSentEvent(event='test', data=json.dumps({'counter': counter}))

# Wait for 0.5 second
await asyncio.sleep(0.5)

print('Generator finished after 10 seconds.')

@app.get('/sse')
async def sse_endpoint_get(request: Request):
event_generator = generator(request)
return EventSourceResponse(event_generator)

@app.post('/sse')
async def sse_endpoint_post(request: Request):
event_generator = generator(request)
return EventSourceResponse(event_generator)

if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8000)
```

2. Run the server:
```bash
python sse.py
```

3. Launch a Quick Tunnel:
```bash
cloudflared tunnel --url http://localhost:8000
```

4. Test both endpoints:
```bash
# without cloudflared
curl -v -X GET http://localhost:8000/sse
curl -v -X POST http://localhost:8000/sse

# over cloudflared
curl -v -X GET https://YOUR_SUBDOMAIN.trycloudflare.com/sse
curl -v -X POST https://YOUR_SUBDOMAIN.trycloudflare.com/sse
```

5. Observe behavior:
- over cloudflared + `POST` → one event every 0.5 seconds, **as expected**
- over cloudflared + `GET` → nothing appears until 10 seconds later (when the server closes the connection), and then **all 20 events arrive at once**

---

### **Expected behavior**
Streaming via SSE over `GET` should deliver events **as they are generated**, not only after the connection is closed. This is core to how SSE is designed to work, and breaking this behavior affects real-time use cases severely.

---

### **Environment and versions**
- OS: Ubuntu 20.04 LTS
- Architecture: AMD64
- cloudflared version 2025.2.1 (built 2025-02-27-1123 UTC)
- Python: 3.11
- Uvicorn: 0.34.0
- FastAPI: 0.115.11
- sse-starlette: 2.2.1

---

### **Logs and errors**
There are no visible errors, but `curl -v` shows that FastAPI + Uvicorn (without cloudflared) responds with all correct headers.

```
< HTTP/1.1 200 OK
< date: Sat, 12 Apr 2025 07:18:52 GMT
< server: uvicorn
< cache-control: no-store
< connection: keep-alive
< x-accel-buffering: no
< content-type: text/event-stream; charset=utf-8
< transfer-encoding: chunked
```

This confirms:
- The application is working properly.
- All relevant headers are already set to disable buffering.
- The delayed flush behavior happens **after the origin**, likely in the tunnel or edge.

---

### **Additional context**
- I haven’t tested this with **named tunnels**, but if the root cause lies within buffering logic in Quick Tunnel or edge infrastructure, it could affect **any tunnel-based deployment**.
- While I used **FastAPI** in this test, it’s worth noting that FastAPI has become a **very common choice for building LLM-based applications** that use SSE for real-time token streaming.
- If the issue is method- or transport-dependent, similar problems may occur in **other languages or frameworks** (Node.js, Go, etc.) as well.

🙏 If this behavior is intended, please clarify and document it. Otherwise, resolving this would make a huge difference for developers relying on real-time SSE streaming.

Thanks again for your work on `cloudflared` — it's an excellent tool that has enabled fast experimentation and deployment for many projects. I'd love to see it continue to improve.

related (?): https://github.com/cloudflare/cloudflared/issues/199

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.