# Set your OpenAI API key here
export OPENAI_API_KEY=sk-...
```

### Traditional request-response

In a traditional request-response model, you would send a single request to the OpenAI API and wait for the entire response to be generated before processing it. Let's see how this looks in practice.

Run the following command to send a request to the OpenAI API and receive a response:

```bash
curl https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Accept: application/json" \
  --silent \
  --no-buffer \
  -d '{
    "model": "gpt-4.1",
    "input": "Write a short sentence about tamborines."
  }'
```

The response will look something like this:

```json
{
  "id": "resp_1234567890abcdef",
  "object": "response",
  "output": [
    {
      "id": "msg_682ddf545708819897cf1cc0c76269780de9aa243ddb5cc3",
      "type": "message",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "annotations": [],
          "text": "Tambourines are handheld percussion instruments that create a jingling sound when shaken or struck."
        }
      ],
      "role": "assistant"
    }
  ]
  # ... snip ...
}
```

We're most interested in the `content` field, which contains the actual text generated by the model. See how the entire response is wrapped in a JSON object? This means that the entire response must be generated before it can be sent to the client.

For this tiny example, that isn't a big deal. But imagine if the model were generating a full-length novel or a long technical document. There would be a significant delay before the user saw any output, and they would have to wait for the entire response to be generated before they could start reading it.

### Streaming

Now let's try the same thing with streaming.

When we use this command, the OpenAI API will send the response in chunks as it is generated, allowing us to start processing it immediately:

```bash
curl https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Accept:text/event-stream" \
  --no-buffer \
  --silent \
  -d '{
    "model": "gpt-4.1",
    "input": "Write a short sentence about tamborines.",
    "stream": true
  }'
```

OpenAI, like many modern APIs, uses Server-Sent Events (SSE) to stream responses. In this format, each piece of data is typically a JSON object, prefixed by `data:`.

The response will look something like this:

```text
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"A"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" tamb"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"our"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"ine"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" is"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" a"}

... we cut a whole bunch of these out for brevity ...

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" music"}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"."}

event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"text":"A tambourine is a small percussion instrument with jingling metal disks, often shaken or struck to add rhythm and sparkle to music."}
```

### This isn't simpler at all

You might look at that SSE output with its `event:` and `data:` lines and think it doesn't immediately scream "simplicity". And you're right, SSE has its own protocol for framing events. It's powerful for sending named events, and many APIs like OpenAI use it effectively.

Each `data:` payload in that SSE stream is a self-contained JSON object. Just like with JSONL, these objects are individual, parsable JSON units.

Let's take a look at how we might process this response in real time:

```bash
curl https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Accept: text/event-stream" \
  --no-buffer \
  --silent \
  -d '{
    "model": "gpt-4.1",
    "input": "Write a 200 word paragraph about tamborines.",
    "stream": true
  }' | stdbuf --output=L awk '
  /response.output_text.delta/ { getline;
    if ($0 ~ /data:/) {
      gsub(/.*"delta":"/, "", $0);
      gsub(/".*/, "", $0);
      printf "%s", $0;
      fflush();
    }
  }'
```

If you run this command (you may need to install `coreutils` if the `stdbuf` command is not found), you'll see the text being generated in real time, one chunk at a time. This is the power of streaming!

<video
  controls={false}
  loop={true}
  autoPlay={true}
  muted={true}
  width="100%"
  className="mt-10"
>
  <source src="/assets/videos/curl-stream.mp4" type="video/mp4" />
</video>

But as you can see, dealing with the SSE wrapper adds some complexity. Now, what if your API wants to stream a sequence of JSON objects without the additional SSE framing? What if you just want to send one JSON object after another as effortlessly as possible? That's precisely where JSONL shines as the "simplest tool".

## Streaming with JSONL

To see this in action, you can use your own Vercel project or deploy one of Vercel's boilerplate projects. For this example, we'll use the [Vercel AI Chatbot](https://github.com/vercel/ai-chatbot) project.

### Deploying the Vercel AI Chatbot

First, let's deploy the AI Chatbot:

- Log in to your Vercel account.
- Click on the **New Project** button.
- Select the **AI Chatbot** template.
- Follow the prompts to deploy the project.

### Gathering your environment variables

In Vercel, create a new token for your account. This token will be used to authenticate your API requests.

- Go to your Vercel account settings.
- Click on the **Tokens** tab.
- Click **Create**.
- Copy the token and save it in a secure place.

Next, we need to get your Vercel project ID and deployment ID:

- Go to your Vercel project dashboard.
- Click on the **Settings** tab.
- Copy the project ID from the URL and save it.
- Now, click on the **Deployments** tab.
- Click on the **Latest Deployment** link.
- Copy the deployment ID from the URL (prefix it with `dpl_`).
- Save the prefixed deployment ID in a secure place.

### Setting up your environment variables

In your terminal, run the following command to set your environment variables:

```bash
export VERCEL_TOKEN=your-vercel-token
export VERCEL_PROJECT_ID=prj_your-vercel-project-id
export VERCEL_DEPLOYMENT_ID=dpl_your-vercel-deployment-id
```

### Streaming your logs

Now that you have your environment variables set up, you can start streaming your Vercel logs.

Run the following command in your terminal:

```bash
curl https://api.vercel.com/v1/projects/$VERCEL_PROJECT_ID/deployments/$VERCEL_DEPLOYMENT_ID/runtime-logs \
  -H "Authorization: Bearer $VERCEL_TOKEN" \
  --no-buffer \
  --silent
```

This starts streaming your Vercel logs in real time. You should see something like this:

```jsonl
{"level":"info","message":"Starting Vercel runtime..."}
{"level":"info","message":"Vercel runtime started."}
{"level":"info","message":"Received request: GET /api/chat"}
{"level":"info","message":"Processing request..."}
{"level":"info","message":"Request processed."}
{"level":"info","message":"Sending response..."}
{"level":"info","message":"Response sent."}
{"level":"info","message":"Request completed."}
{"level":"info","message":"Vercel runtime stopped."}
```

Now this looks familiar, right? Each line is a complete JSON object, and you can process it line by line as it arrives.

If you use your app, you should see the logs being streamed in real time.

Now we can use [jq](https://stedolan.github.io/jq/) to filter the logs and extract the messages:

```bash
curl https://api.vercel.com/v1/projects/$VERCEL_PROJECT_ID/deployments/$VERCEL_DEPLOYMENT_ID/runtime-logs \
  -H "Authorization: Bearer $VERCEL_TOKEN" \
  --no-buffer \
  --silent | jq -r 'select(.level == "error") | .message'
```

This filters the logs and only shows the `error`-level messages. Using `jq` is a great way to process JSONL data in real time, and it works perfectly with streaming data.

The output should look something like this:

```text
⨯ [Error: No response is returned from route handler '/vercel/path0/app/(chat)/api/chat/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.]
⨯ [Error: No response is returned from route handler '/vercel/path0/app/(chat)/api/chat/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.]
⨯ [Error: No response is returned from route handler '/vercel/path0/app/(chat)/api/chat/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.]
```

This is way easier than trying to parse an entire JSON object with a complex structure. We're querying a stream of JSON objects with the simplest of tools in real time.

## Generating JSONL on the server side

You've seen how powerful consuming JSONL streams can be, especially with tools like `jq`. But what about producing them from your own API? Good news: it's often simpler than constructing and managing large, in-memory JSON arrays.

These are the core principles an API producer should follow when streaming JSONL:

- **Set the correct headers:** Make sure to set the `Content-Type` header to `application/jsonl`.
- **Iterate and Serialize:** Process your data record by record (for example, from a database query, an LLM token stream, or any other iterable source). Convert each record into a JSON string.
- **Write, newline, flush, repeat:** Write the JSON string to the response output stream, follow it with a newline character (`\n`), and flush the output buffer. Flushing ensures the data is sent to the client immediately, rather than being held back by the server or application framework.

### Python example

Let's illustrate these principles with a simple Python Flask example. Flask is a lightweight web framework that makes streaming straightforward.

```python filename="app.py"
from flask import Flask, Response
import json
import time

app = Flask(__name__)