The short version: The OpenAI Responses API gives you three ways to remember a conversation, and they are not interchangeable. previous_response_id chains calls with almost no code — but stored responses expire after 30 days by default, so a thread a user reopens next month is gone. The Conversations API holds a server object with no TTL. Rolling your own — resending the message list from your database — is the only path that gives you portability, your own retention rules, and editable history. Pick by how long the thread must live and who needs to own it.

The default that quietly forgets#

previous_response_id is the path the migration guide nudges you toward, and for good reason — it is one field:

first = client.responses.create(model="gpt-4o", input="What's my plan's storage limit?")
# ...later in the same session...
second = client.responses.create(
    model="gpt-4o",
    previous_response_id=first.id,
    input="And if I upgrade?",
)

You send only the new turn; OpenAI reconstructs the history from the stored response. No tokens resent, no bookkeeping. The catch is in three words: stored for 30 days. Stored responses carry a 30-day TTL by default. If your product lets a user walk away and come back — a support inbox, a saved chat, a project assistant — the chain is silently broken the day the oldest response ages out, and the model answers your returning user as a stranger.

previous_response_id is perfect right up until someone reopens the thread on day 31.

The Conversation object: same convenience, no expiry#

The Conversations API fixes exactly that. You create a conversation once and attach it to every call:

conv = client.conversations.create()
client.responses.create(model="gpt-4o", conversation=conv.id,
                        instructions=SYSTEM, input="What's my plan's storage limit?")
# next week, same conversation:
client.responses.create(model="gpt-4o", conversation=conv.id, input="And if I upgrade?")

History still lives on OpenAI's side, so you still resend nothing. But conversation items are not subject to the 30-day TTL — they persist until you delete them. You pay for that with one more object to create, store the id of, and clean up when a user deletes their account. That is a fair trade for any thread that must outlive a month.

Rolling your own: the only portable answer#

Both server-side options share one weakness: the history lives on OpenAI, in OpenAI's shape, under OpenAI's rules. When conversation history is a product surface — users browse it, search it, export it — or when compliance demands your own retention and redaction, you store the messages yourself and resend them:

history = load_from_db(conversation_id)          # your list of {role, content}
history.append({"role": "user", "content": user_msg})
resp = client.responses.create(model="gpt-4o", input=history)
save_to_db(conversation_id, history, resp.output_text)

This is the expensive option — you pay input tokens for the whole history on every turn, and you write the storage and resend logic yourself. In return you get the three things the server options cannot give you: portability (it is your data; point it at any model tomorrow), control (your TTL, your redaction, your edits), and auditability. If store=false is on your calls for privacy reasons, you are already here by necessity — there is no server copy to chain from.

The decision, in one pass#

The mistake is treating these as a ranking where one wins. They are a division of labor sorted by how long the thread must live and who has to own it. Match the mechanism to the lifetime, and the 30-day trap never touches a user.