| name | dspy-parallel |
| description | Use when you have independent LM calls that can run concurrently — batch processing, fan-out patterns, or speeding up pipelines with no data dependencies between steps. Common scenarios - processing a batch of inputs through a DSPy module concurrently, fan-out patterns where multiple independent LM calls run at once, speeding up evaluation by parallelizing predictions, or reducing wall-clock time for pipelines with no data dependencies. Related - ai-building-pipelines, ai-serving-apis. Also used for dspy.Parallel, concurrent LM calls, batch processing in DSPy, parallel DSPy execution, speed up DSPy pipeline, fan-out LM calls, concurrent predictions, parallelize evaluation, async DSPy calls, reduce latency with parallel execution, batch inference DSPy, process multiple inputs at once, throughput optimization, run DSPy modules concurrently, parallel map over inputs. |
Run LM Calls in Parallel with dspy.Parallel
Guide the user through using DSPy's Parallel module to execute multiple LM calls concurrently. dspy.Parallel is the built-in way to speed up batch processing and fan-out patterns without writing threading code yourself.
What is dspy.Parallel
dspy.Parallel takes a list of (module, inputs) pairs and executes them concurrently using a thread pool. It handles threading, progress bars, error limits, and timeouts so you don't have to.
Use it when you have:
- A batch of inputs to run through the same module (classify 500 tickets, summarize 100 articles)
- Multiple independent modules to run on the same input (sentiment + topics + entities at once)
- Any set of LM calls that don't depend on each other
If call B depends on the result of call A, those two calls must be sequential. Everything else can be parallel.
Step 1 — Clarify the use case
Before writing code, ask:
- Same module or mixed? Are you running one module on many inputs (batch), or multiple different modules on the same input (fan-out)? Same-module batches can use either
dspy.Parallel or the simpler .batch() method (see below). Fan-out requires dspy.Parallel.
- How many items? Fewer than ~5 items — just call the module directly; parallelism overhead is not worth it. 5–100 items —
dspy.Parallel with 4-8 threads. 100+ items — increase threads up to your provider rate limit.
- Any data dependencies? If any call needs the output of a previous call, those must stay sequential. Confirm all pairs are truly independent before parallelizing.
- Error tolerance? If any failure should abort the batch, use
max_errors. If you want partial results, set return_failed_examples=True.
Basic usage
Pass a list of (module, inputs) pairs. Each pair is one unit of work:
import dspy
lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)
classify = dspy.Predict("text -> label: str")
texts = [
"I love this product!",
"Terrible experience, want a refund.",
"It's okay, nothing special.",
"Best purchase I've made this year.",
]
exec_pairs = [(classify, {"text": t}) for t in texts]
parallel = dspy.Parallel(num_threads=4)
results = parallel(exec_pairs)
for text, result in zip(texts, results):
print(f"{text[:30]:30s} -> {result.label}")
results is a list in the same order as exec_pairs, so results[i] corresponds to exec_pairs[i].
Constructor options
dspy.Parallel(
num_threads=4,
max_errors=5,
return_failed_examples=False,
provide_traceback=None,
disable_progress_bar=False,
timeout=120,
)
| Parameter | Type | Default | Purpose |
|---|
num_threads | int | None | None | Number of concurrent threads. Falls back to dspy.settings.num_threads. |
max_errors | int | None | None | Stop execution after this many errors. Falls back to dspy.settings.max_errors. |
access_examples | bool | True | Unpack Example objects via .inputs(). Set False to pass raw Examples. |
return_failed_examples | bool | False | When True, return failed examples separately instead of raising. |
provide_traceback | bool | None | None | Include Python tracebacks for failed examples. |
disable_progress_bar | bool | False | Suppress the progress bar. |
timeout | int | 120 | Max seconds per individual task. |
straggler_limit | int | 3 | Threshold for flagging slow-running tasks. |
.batch() — simpler alternative for same-module batching
Every dspy.Module has a built-in .batch() method that handles the most common case: running the same module on many inputs. If you do not need to mix different modules per item, .batch() is less code than dspy.Parallel:
classify = dspy.Predict("text -> label: str")
texts = ["Great!", "Terrible.", "Meh."]
examples = [dspy.Example(text=t).with_inputs("text") for t in texts]
results = classify.batch(examples, num_threads=4)
for r in results:
print(r.label)
.batch() accepts all the same concurrency parameters as dspy.Parallel:
results = classify.batch(
examples,
num_threads=4,
max_errors=5,
return_failed_examples=True,
provide_traceback=None,
timeout=120,
)
When to use .batch() vs dspy.Parallel:
| Need | Use |
|---|
| Same module, many inputs | .batch() — simpler API |
| Different modules per input (fan-out) | dspy.Parallel — required |
| Mix modules and share an exec loop | dspy.Parallel — more control |
Configuring concurrency
Start with a thread count that matches your rate limits, not your CPU cores. LM calls are I/O-bound (waiting on HTTP responses), so you can safely use many threads:
parallel = dspy.Parallel(num_threads=4)
parallel = dspy.Parallel(num_threads=16)
parallel = dspy.Parallel(num_threads=8)
If you hit rate-limit errors (HTTP 429), reduce num_threads or add retry logic in your LM configuration.
Input formats
Parallel accepts inputs as dictionaries, dspy.Example objects, or tuples:
module = dspy.Predict("question -> answer")
pairs = [(module, {"question": "What is DSPy?"})]
example = dspy.Example(question="What is DSPy?").with_inputs("question")
pairs = [(module, example)]
parallel = dspy.Parallel(num_threads=2)
results = parallel(pairs)
Aggregating results
Results come back as a list. Aggregate however your application needs:
import dspy
classify = dspy.Predict("text -> label: str, confidence: float")
texts = ["Great!", "Terrible.", "Meh.", "Amazing!", "Awful."]
parallel = dspy.Parallel(num_threads=4)
results = parallel([(classify, {"text": t}) for t in texts])
from collections import Counter
label_counts = Counter(r.label for r in results)
print(label_counts)
high_confidence = [
(text, r.label)
for text, r in zip(texts, results)
if r.confidence > 0.8
]
output = [
{"text": t, "label": r.label, "confidence": r.confidence}
for t, r in zip(texts, results)
]
Error handling
By default, Parallel raises an exception after max_errors failures. To handle errors gracefully, use return_failed_examples=True:
parallel = dspy.Parallel(
num_threads=4,
max_errors=10,
return_failed_examples=True,
provide_traceback=True,
)
results, failed_examples, exceptions = parallel(exec_pairs)
When return_failed_examples=True, the return value is a 3-tuple:
results -- list of successful predictions (same length as successes)
failed_examples -- list of (module, inputs) pairs that failed
exceptions -- list of exceptions corresponding to each failure
Handle failures after the batch completes:
results, failed, errors = parallel(exec_pairs)
print(f"Succeeded: {len(results)}, Failed: {len(failed)}")
if failed:
fallback = dspy.ChainOfThought("text -> label: str")
retry_pairs = [(fallback, inputs) for _, inputs in failed]
retry_results = parallel(retry_pairs)
Setting an error budget
Use max_errors to fail fast when too many calls are failing (e.g., provider outage):
parallel = dspy.Parallel(num_threads=4, max_errors=5)
try:
results = parallel(exec_pairs)
except Exception as e:
print(f"Batch aborted: {e}")
Timeouts
The timeout parameter sets a per-task time limit in seconds. Tasks that exceed this are terminated:
parallel = dspy.Parallel(num_threads=4, timeout=60)
Using different modules per item
Each pair can use a different module. This is useful for fan-out patterns where you run multiple analyses on the same input:
import dspy
sentiment = dspy.Predict("text -> sentiment: str")
topics = dspy.Predict("text -> topics: list[str]")
summary = dspy.ChainOfThought("text -> summary: str")
text = "DSPy is a framework for programming language models..."
exec_pairs = [
(sentiment, {"text": text}),
(topics, {"text": text}),
(summary, {"text": text}),
]
parallel = dspy.Parallel(num_threads=3)
results = parallel(exec_pairs)
combined = {
"sentiment": results[0].sentiment,
"topics": results[1].topics,
"summary": results[2].summary,
}
When to use Parallel vs a sequential loop
| Scenario | Use | Why |
|---|
| Process 100+ items through the same module | Parallel | Massive speedup from concurrent HTTP requests |
| Run 3 independent analyses on one input | Parallel | All three calls happen at once |
| Pipeline where step 2 needs step 1's output | Sequential loop | There's a data dependency |
| Single LM call | Neither | No benefit from parallelism |
| Processing 2-3 items | Either works | Overhead is negligible either way |
Sequential loop (before)
results = []
for text in texts:
result = classify(text=text)
results.append(result)
Parallel (after)
parallel = dspy.Parallel(num_threads=8)
results = parallel([(classify, {"text": t}) for t in texts])
For a batch of 100 items with ~1 second per LM call:
- Sequential: ~100 seconds
- Parallel (8 threads): ~13 seconds
Parallel inside a module
Wrap Parallel usage inside a dspy.Module for clean composition:
class BatchClassifier(dspy.Module):
def __init__(self, num_threads=4):
self.classify = dspy.Predict("text -> label: str, confidence: float")
self.num_threads = num_threads
def forward(self, texts: list[str]):
parallel = dspy.Parallel(num_threads=self.num_threads)
exec_pairs = [(self.classify, {"text": t}) for t in texts]
results = parallel(exec_pairs)
return dspy.Prediction(
labels=[r.label for r in results],
confidences=[r.confidence for r in results],
)
classifier = BatchClassifier(num_threads=8)
result = classifier(texts=["Great!", "Terrible.", "Meh."])
print(result.labels)
This keeps the parallelism as an implementation detail. Callers don't need to know about threading -- they just pass a list and get a list back.
Gotchas
- Claude writes a
for loop instead of using dspy.Parallel. When asked to process a batch of inputs, Claude defaults to a sequential loop. For any batch of 5+ independent LM calls, use dspy.Parallel — it is dramatically faster because LM calls are I/O-bound.
- Claude sets
num_threads to match CPU cores. LM calls are network-bound (waiting on HTTP responses), not CPU-bound. Thread count should match your provider rate limit, not your CPU count. 8-16 threads is typical even on a 4-core machine.
- Claude forgets that
return_failed_examples=True changes the return type. Without it, parallel(pairs) returns a flat list. With it, it returns a 3-tuple (results, failed_examples, exceptions). Destructure accordingly or the code will break.
- Claude nests Parallel inside Parallel without considering total concurrency. An inner
Parallel(num_threads=3) inside an outer Parallel(num_threads=4) creates up to 12 concurrent LM calls. This can exceed provider rate limits. Calculate the total: outer_threads * inner_threads.
- Claude uses
dspy.Parallel for 1-2 items. The threading overhead is not worth it for fewer than ~5 items. Just call the module directly.
- Skipping result order verification.
dspy.Parallel preserves input order — results[i] corresponds to exec_pairs[i]. Verify this with a quick sanity check: zip results with your original inputs and confirm the first few items align before processing the full batch.
Cross-references
Install any skill: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>
- Modules are the building blocks you pass to Parallel -- see
/dspy-modules
- Batch classification (sort items into categories at scale) -- see
/ai-sorting
- Multi-step pipelines that combine sequential and parallel stages -- see
/ai-building-pipelines
- Async API serving (non-blocking DSPy in FastAPI / asyncio) -- see
/ai-serving-apis
- Evaluation uses its own threading via
num_threads -- see /dspy-evaluate
- For worked examples (batch classification, multi-aspect analysis), see examples.md
- Install
/ai-do if you do not have it — it routes any AI problem to the right skill and is the fastest way to work: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-do
Additional resources