We all use the chat box like a draft. We type, we change our mind, we delete, and we think nothing goes anywhere until we press Enter.

Some days ago I was teaching my intern about SSE (Server-Sent Events). We talked about the different ways a server can push data to the client, like SSE, WebSockets and RPC streaming, and I said that Claude Code and some AI chat tools use SSE to stream the answer. So I opened ChatGPT with DevTools to show it. And yes, the answer came back as text/event-stream. But before I even sent my message, I saw some requests to an endpoint called prepare. I didn’t send anything yet! So I wanted to know what is really going on. I recorded a full session, every key I pressed and every request, and checked what goes out and when.

Short answer: the first letter you type is sent to OpenAI about 20 milliseconds after you type it. After that, the text in the box is sent again every 45 seconds while you are editing. Each of these requests reserves a specific server for you for 60 seconds, and when you finally send your message, it takes the token for that server with it.

How I tested it

DevTools shows the requests, but it doesn’t show which key caused which request. So before typing, I pasted a small script in the Console on chatgpt.com. It wraps window.fetch and saves the body, headers and response of every conversation request. It also saves every change in the message box with a timestamp. Before saving anything, it removes the authorization header, cookies, device IDs and anti-bot tokens.

To be sure the script didn’t miss anything, I also checked the browser’s Resource Timing list. It showed 15 matching requests, and my script had all 15.

My setup: Chrome 156 on macOS, a normal chat (not a temporary chat), model on Auto, 26 October 2026. Then I did this:

  1. I typed Hello letter by letter, waited, and deleted it.
  2. I typed My fake secret is 12345, waited about 15 seconds, and deleted it.
  3. I left the box empty for more than one minute.
  4. I typed سلام (“hello” in Persian) and pressed Enter.

What went out

Each blue dot is one prepare request. The gray line shows how many characters were in the message box. Move your mouse on the chart to see the text in the box.

4 requests went out before I pressed Enter, and one more right after it:

RequestWhat was sent
#1 at 19.6 s
my first key, +20 ms
"H"
#2 at 64.6 s
45.000 s after #1
"My fake secret is 12345"
#3 at 111.3 s
45.0 s after my next edit
nothing, the box was empty
#4 at 195.3 s
first key, +19 ms
"س"
Enter at 204.8 s
the real message
سلام to /f/conversation
#5 at 207.7 s
2.9 s after Enter
nothing

This is the same session in the Network tab (filter: “prep”):

Chrome DevTools Network tab filtered by "prep", showing six prepare requests with status 200
The first four rows are requests #1 to #4. The 2.49 s row is another endpoint (/sentinel/chat-requirements/prepare), and the last row is request #5.

The first letter goes out right away

Request #1 had only one letter: H. It was sent 20 milliseconds after I pressed the key. Later the same thing happened with س, after 19 milliseconds. So ChatGPT doesn’t wait for you to stop typing. If nothing was sent recently, your first letter goes out immediately.

After that, every 45 seconds

Requests #1 to #4 have this field: client_prepare_dispatch: "debounced". Usually “debounced” means “wait until the user stops typing”. But the timing shows something else. When I typed and deleted Hello and then typed the fake secret, I made 33 edits in 28 seconds, and sometimes I stopped for up to 16 seconds. Nothing was sent. Then request #2 was sent exactly 45.000 seconds after request #1, and 16.6 seconds after I stopped typing. It had whatever was in the box at that moment.

I wanted to be sure this was not random. So I wrote a small simulation: I gave my recorded edits to three different timing rules and compared the results with the real requests:

Plain text
real prepare requests before Enter:  19588  64588  111279  195305 ms
throttle, 45 s:                      19602  64602  111262  195319 ms   4 of 4, max 17 ms off
debounce, 1 s pause:                 6 requests at wrong times
debounce, 500 ms pause:              11 requests at wrong times

The 45 second throttle matches all four requests, with max 17 ms difference. (This small difference is because my script used the browser input events as the clock.) The rule is simple: send on the first change, then send at most once every 45 seconds with the latest text. This is exactly how throttle works in lodash. And lodash builds throttle from debounce with a maxWait option, so maybe this is why the field says “debounced”.

What did the server get?

For privacy, this is the most important part. And it’s not as simple as “everything you type is sent”:

  • Hello: I typed it and deleted it between two requests. Only the first letter, H, left my browser.
  • My fake secret is 12345: it was still in the box when the 45 second timer fired, so the full text was sent. I started deleting it 1.7 seconds later. Too late!
  • When the box was empty, the next request didn’t have partial_query at all. So the server also knew that the draft was deleted.

So if your deleted text reaches OpenAI or not, depends on timing. The first letter always goes. And anything that is still in the box when the timer fires also goes.

Also, these requests are not anonymous. Every prepare request has the same authorization header and chatgpt-account-id as a normal message. And all four requests before Enter had the same x-oai-turn-trace-id as the message I finally sent. So on the server side, these drafts can be connected to my account and to my real message. What OpenAI does with them (do they log them? for how long? do they treat them like sent messages?) I can’t see from outside.

What is inside a prepare request

This is the body of request #2. I made the message ID shorter:

JSON
{
  "client_prepare_state": "sent",
  "action": "next",
  "model": "auto",
  "parent_message_id": "client-created-root",
  "timezone": "Europe/Istanbul",
  "timezone_offset_min": -180,
  "is_do_not_remember": false,
  "local_function_names": [
    "local.continue_in_work"
  ],
  "partial_query": {
    "author": {
      "role": "user"
    },
    "content": {
      "content_type": "text",
      "parts": [
        "My fake secret is 12345"
      ]
    },
    "id": "b60b…b80e"
  },
  "client_prepare_dispatch": "debounced",
  "client_prepare_source": "composer_editor_state"
}

It looks like a real message request: action, model, parent message, timezone. The only difference is that the draft is in partial_query, not in messages. And client_prepare_source: "composer_editor_state" means the request came from the state of the text box, not from a button.

A server reserved for 60 seconds

Request #1 had no token. But its response had one:

JSON
{"status": "ok", "conduit_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb25kdWl0X3V1aWQiOi…"}

After that, every prepare request sent the latest token back in an x-conduit-token header, and every response gave a new one. The token is a JWT. The payload of a JWT is only Base64url encoded. It’s signed (ES256) but not encrypted, so anyone who has it can read it. You can decode it in the Console. Please don’t paste it into a random JWT website:

JavaScript
const token = '…'; // the conduit_token value
JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')))

This is the first token. I hid the IDs:

JSON
{
  "conduit_uuid": "455d…b5af",
  "conduit_location": "10.x.x.x:8307",
  "cluster": "unified-189",
  "iat": 1793010064,
  "exp": 1793010124,
  "turn_topic_id": null
}
  • conduit_location is a private IP address and port. It’s one specific machine inside OpenAI’s network.
  • cluster is the name of the group of machines.
  • exp minus iat is 60. So the token is valid for 60 seconds. All tokens in my session had the same 60 seconds.

When I followed the token during the session, I saw how the reservation stays alive:

  • Request #2 sent the first token back while it was still valid. It got a new token with the same conduit ID, valid for another 60 seconds. Request #3 did the same.
  • Request #4 came 84 seconds after #3, so its token was already expired (24 seconds before). The server gave a new conduit ID, but on the same machine and in the same cluster.
  • My real message (to /f/conversation) had this new token and client_prepare_state: "success". Its response renewed the token one more time.

Here is the same session, but this time with the tokens. Press Play to see it again:

Each bar is one token. It starts when my browser got it and it is valid for 60 seconds (exp minus iat). The label shows which request got it, and "msg" is my real message. The gray text above the bars is the conduit ID. Move your mouse on a bar or a dot to see more.

45 seconds is less than 60 seconds, so while you keep typing, the reservation never expires. I don’t think this is random, but I can’t prove it from outside.

It also gets ready for your next message

Request #5 was sent 2.9 seconds after I pressed Enter. The answer was still coming! It had client_prepare_dispatch: "immediate" and client_prepare_source: "context_change", the new conversation ID, and no draft. It got a reservation on another machine in the same cluster. So before I even read the answer, a machine was already waiting for my next message.

Why do they do this?

OpenAI didn’t write anything about this endpoint, so this part is my guess.

I think the reason is speed. Before you see the first word of the answer, a lot of work must happen. Your request must go to a machine that has free capacity. Then the context must be ready: the system prompt, your custom instructions, memory, tools. Most of this doesn’t depend on what you are going to write. So if they reserve a machine on your first key and keep it alive while you type, they can do this work before you press Enter. And the conduit token is probably how your final message finds the same machine.

Does the server also start to process the partial text? I don’t know. But the draft is there, so it’s possible.

There are also some hints that the server may decide other things early. Every prepare request had oai-genui-client-actions: launch_ecosystem_widget,trigger_agentic_handoff, and the body had local.continue_in_work. These look like a list of things the client can do. So maybe the server decides early if a widget or a hand-off to another mode is needed. But this is only a guess from the names.

Is this bad?

Speed is good. Nobody likes to wait for an empty answer. But most people think the chat box is private until they press Enter, and here this is not completely true. Your first letter always goes to OpenAI. And the text that is in the box when the 45 second timer fires also goes, connected to your account, even if you delete it one second later.

To be fair, ChatGPT is not the only one. Search engines send your keys for autocomplete, and email apps do the same for smart compose. It’s a normal way to make apps feel fast.

The difference is what we expect. With a search box, we kind of know the suggestions come from a server. With a chat box, most people think nothing happens before Send. I think people should know how their tools really work.

And for me, honestly, it’s not fine. When I press Enter, I decide what I share. Here a 45 second timer decides for me. My fake secret was sent only because it was in the box at the wrong moment. In real life it can be a password that I pasted in the wrong window, a name that I decided not to share, or a message that I wrote when I was angry and then deleted. These drafts go with my account ID and with the same trace ID as my real message, and I can’t see if OpenAI keeps them or for how long.

Also, the reservation doesn’t even need my text. Request #3 had no draft at all, and it still kept the reservation alive. So the text is sent for something else, and I don’t know what. I want my draft to stay in my browser until I press Enter.

Something strange in the headers

I used a normal Chrome tab. But every request had originator: Codex Browser, x-openai-web-frontend: codex_webview and x-openai-codex-window-type: browser, and the message body had "app_surface": "codex_browser". Maybe the ChatGPT website now uses the same frontend as Codex (OpenAI’s coding product), or maybe my account is in an experiment. I don’t know which one.

What I didn’t test

  • This is one session, one account, one browser, on 26 October 2026. The 45 seconds, the 60 seconds, or the whole endpoint can change, or be different for other accounts.
  • I tested a normal chat with the Auto model. I didn’t test a temporary chat or other models yet.
  • I can only see what my browser sends. I can’t see what the server keeps.

Try it yourself

Don’t trust me, check it yourself:

  1. Open chatgpt.com in Chrome and start a new chat.
  2. Open DevTools (F12 or Cmd+Option+I) and go to the Network tab.
  3. Write conversation/prepare in the filter box. If you write only prepare, you will also see an unrelated sentinel request.
  4. Type one letter. A request should appear almost immediately.
  5. Type more, then wait and don’t send. The next request should come about 45 seconds after the first one.
  6. Click on a request. In the Payload tab you can see partial_query, and in the Response tab you can see the conduit_token.

Final words

I only wanted to show SSE to my intern, and I found this. At first it was just some strange requests in DevTools. But actually it’s a smart system. ChatGPT reserves a machine on your first key, keeps it alive with a new request every 45 seconds while you edit, sends your message with that machine’s token when you press Enter, and reserves the next one before the answer is finished.

I understand why they built it. But for me, speed is not a good reason to send my drafts. In modern web apps, “not sent” doesn’t always mean “not seen”. If you are going to type something you are not sure about, remember that the text box may already be sending it.

— Abolfazl Abbasi

Join the conversation

Questions, corrections, and a different way of looking at it are welcome.