Parallelizing OpenAI Agent Systems
Introduction & Goals
Welcome to the first lesson of Parallelizing OpenAI Agentic Systems in Ruby! In this lesson, you'll learn how to use Ruby's threading capabilities to run multiple concurrent conversations with gpt-5. You've already seen the Agent class that can handle conversations, use tools, and hand off control to other specialized agents.
To keep the core concurrency concept easier to retain, this lesson is split into three checkpoints:
- Agent prerequisites: the small parts of
Agentyou need before adding threads. - Thread orchestration: the main delta — creating
Threadobjects, collectingThread#value, and keeping client state isolated. - Concurrency limitations: what still runs sequentially, including per-turn tool loops and handoffs.
Focus most of your attention on the thread orchestration pattern. The Agent internals are included only as prerequisites so you can see why concurrent runs are safe.
Why Concurrent Execution Matters for Agent Systems
Let's start by understanding why we need concurrency in the first place. When you make a regular API call to OpenAI's Responses API, your program stops and waits for the response. This is called blocking behavior. If you need to have three separate conversations with gpt-5, your program handles them one at a time: start conversation 1, wait for all responses, finish conversation 1, then start conversation 2, and so on.
This sequential approach wastes time. While your program waits for gpt-5 to respond to conversation 1, it could be starting conversation 2 or 3. Network calls and API processing take time, but your CPU sits idle during these waits.
Concurrent execution using Ruby threads solves this problem. When you create a thread for an API call, your program can continue creating more threads and starting other conversations while waiting for responses. Think of it like a restaurant: a sequential waiter takes one order, goes to the kitchen, waits for the food, delivers it, and only then takes the next order. A concurrent approach is like having multiple orders in flight — while one meal is being prepared, other orders are being taken and other meals are being delivered.
This concurrent approach is particularly effective for I/O-bound operations like API calls, network requests, and database queries, where most of the time is spent waiting rather than computing. For CPU-heavy tasks that require intense computation, threads won't provide the same benefits since Ruby threads (depending on your Ruby implementation) may not run truly in parallel for CPU-bound work. However, for network I/O like API calls to gpt-5, threads allow you to overlap the waiting time.
For agent systems, this means a single agent can manage multiple conversations simultaneously by launching each conversation in its own thread. This becomes especially powerful when you need to process multiple independent user requests or run several agents in parallel. Let's see how the provided code implements this pattern.
Agent prerequisites: Understanding the Agent Class Structure
Before we dive into concurrent execution, let's understand the key parts of the Agent class that make concurrent conversations possible.
The Developer Prompt
Unlike a typical system prompt, the Agent class uses a developer prompt that prepends a base instruction set explaining the agent's agentic behavior:
Your custom system_prompt is appended to this base, and the result is injected as the first message with role: "developer" in every request.
Structured Input Messages
The OpenAI Responses API expects a specific message format. The Agent class provides a text_message helper to build these structured input messages:
Each message is a hash containing a role and a content array of typed content blocks. The "input_text" type is used for regular text exchanges — whether from the "developer", "user", or "assistant".
Building and Sending Requests
The build_request_args method assembles the full payload sent to the API:
Notice that the developer prompt is always prepended as the first element of the input array, followed by the conversation history.
A crucial detail is where the OpenAI::Client is instantiated. Rather than creating it once during initialization and sharing it across all threads, a fresh client is created inside create_response for every API call:
This means each API call — and each concurrent thread — uses its own isolated HTTP client instance, avoiding any shared state between threads.
Detecting Tool Calls and Text Responses
After receiving a response, the run method inspects response.output to decide what to do next:
If any function_call items are found in the output, the agent processes them and loops. Tool results are returned as function_call_output messages:
If no function_call items are found, the agent returns the final text response via response.output_text and exits the loop.
Setting Up Concurrent Execution
Now let's look at how to set up multiple concurrent conversations. The provided main.rb shows the pattern. First, we define multiple prompts that we want to process:
Next, we create a single agent instance that will handle all conversations:
This agent doesn't have any tools or tool_schemas configured in this example — it will rely purely on gpt-5's built-in mathematical reasoning capabilities and answer directly without calling any tools. If you wanted to provide specific tools (like calculator functions), you would pass them via the tools: and tool_schemas: parameters as supported by the Agent class.
Thread orchestration: Creating and Running Concurrent Threads
With our agent and prompts ready, we can now create threads to run multiple conversations concurrently:
This code creates one thread for each prompt using Thread.new. Inside each thread's block, we call agent.run() with a single user message built by the text_message helper:
The main.rb file defines its own top-level version of this helper so message creation stays consistent with the Agent class internals. The map operation returns an array of thread objects, and importantly, the threads have already started executing at this point.
The key insight here is that each thread runs its own conversation independently. While one thread waits for gpt-5's response to the compound interest question, another thread can be waiting for the x^3 + 2x^2 - 5 derivative question, and a third can be waiting for the 3x + 15 = 45 equation solving question. Ruby's thread scheduler handles switching between threads efficiently.
To collect the results, we wait for all threads to complete:
The Thread#value method blocks until the thread completes and returns the value returned by the thread's block (in this case, the result of agent.run()). By calling threads.map(&:value), we wait for all threads to finish and collect all their results in order. This is the synchronization point where the main thread waits for all conversations to complete.
Finally, we display the results:
Note that agent.run() returns a tuple of [messages, text], so we destructure it and display only the final text response.
Entry Point and Execution Model
Running this concurrent agent system is straightforward — it's just a regular Ruby script:
When Ruby executes this script, it:
- Loads the required classes and creates the
agent - Creates and immediately starts all
threads(inprompts.map { Thread.new { ... } }) - Continues to the
threads.map(&:value)line, which blocks waiting for allthreads - Once all
threadscomplete, displays the results and exits
The concurrency happens automatically between steps 2 and 3. While the main thread is blocked at threads.map(&:value), Ruby's thread scheduler switches between the worker threads, allowing them to make progress on their OpenAI Responses API calls.
Each thread independently calls agent.run(), which internally calls create_response, which instantiates a fresh OpenAI::Client for that specific request. This means the three concurrent conversations each use their own separate HTTP client — no connection sharing occurs between threads.
This is different from a purely sequential approach where you might have:
In the sequential version, each conversation would complete fully before the next one started, wasting time during network I/O waits.
Observing Concurrent Execution
When you run the concurrent version, you'll see output like this:
The key observation is that all three conversations happen concurrently, even though the output is displayed sequentially (because we wait for all threads to complete before displaying results). The timing line confirms the total elapsed time — because the API wait times overlap, the three conversations complete in roughly the time it would take for just one, rather than three times as long.
If the agent were configured with tools, you might see interleaved tool call logs from different threads:
The output would show tool calls from different conversations intermixed:
This interleaving demonstrates that the conversations are truly running concurrently — tool calls from different threads are being processed as each thread makes progress.
Concurrency limitations: Benefits and Remaining Bottlenecks
Now that we've seen concurrent execution in action, let's reflect on what we've achieved and what limitations remain.
What We Gain
By wrapping multiple agent.run() calls in separate Ruby threads, we overlap the I/O wait time across independent conversations. Each thread calls create_response, which fires off an HTTP request to the OpenAI Responses API and then blocks while awaiting the response. While one thread is blocked waiting, Ruby's scheduler gives CPU time to other threads that are ready to make progress. For three math problems processed concurrently, the total elapsed time is roughly that of the single slowest request, not the sum of all three.
Within a Single Conversation
Inside one call to agent.run(), operations are sequential. When gpt-5 requests multiple tool uses in a single response turn, the agent processes them one at a time in the loop:
If gpt-5 returns five function_call items in one turn, they execute sequentially within that thread, even though other conversations are running in parallel threads.
Handoff Blocking
If an agent transfers control to another agent via a handoff, the current thread blocks completely while the target agent runs its full conversation:
Handoffs are synchronous within a thread — they do not launch a new thread.
Thread Safety: Fresh Client Per Request
The provided code deliberately sidesteps the most common concurrency pitfall around shared HTTP client state. Rather than creating one OpenAI::Client at initialization and sharing it across all threads, a new client is created inside create_response on every call:
This means each thread operates with a fully independent client instance, including its own connection pool and internal state. You don't need to worry about threads stepping on each other's HTTP connections or corrupting shared client state.
Ruby Runtime Behavior
The degree of true parallelism depends on your Ruby implementation. MRI (the standard Ruby) uses the Global VM Lock (GVL), which prevents true parallel execution of Ruby code. However, for I/O-bound operations like API calls, the GVL is released during network waits, so you still get meaningful concurrency benefits. Alternative Ruby implementations like JRuby or TruffleRuby can provide true parallel thread execution even for CPU-bound work.
In the next lessons, we'll explore patterns for parallelizing tool execution within a single conversation and coordinating multiple specialized agents concurrently.
Summary & Exercises
You've successfully learned how to use Ruby threads to run multiple concurrent conversations with gpt-5 through the OpenAI Responses API. The key pattern is:
- Create multiple
threadsusingThread.new { agent.run([text_message("user", prompt)]) } - Each
threadruns an independent conversation with its own freshOpenAI::Client - Collect results with
threads.map(&:value)to wait for completion
This threading approach allows you to overlap I/O wait time across multiple conversations, dramatically improving throughput when processing multiple independent requests.
In the upcoming practice, you will apply these concepts by building a system that processes a collection of diverse prompts in parallel using the provided Agent implementation and text_message helper.
