Superstep parallelism: a slow sibling appears to block downstream progress of an unrelated fast branch #1013

Closed
opened 2026-02-20 17:42:44 -05:00 by yindo · 7 comments
Owner

Originally created by @atulrawat on GitHub (Oct 21, 2025).

Originally assigned to: @casparb on GitHub.

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Example Code

import asyncio, time
from typing_extensions import Annotated
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from operator import add

class S(TypedDict):
    log: Annotated[list[str], add]

def now():
    return f"{time.perf_counter():.3f}s"

async def fast(_s: S) -> dict:
    await asyncio.sleep(0.10)  # ~100 ms
    return {"log": [f"{now()} FAST done"]}

async def slow(_s: S) -> dict:
    await asyncio.sleep(1.20)  # ~1.2 s
    return {"log": [f"{now()} SLOW done"]}

async def after_fast(_s: S) -> dict:
    return {"log": [f"{now()} AFTER_FAST ran (should run as soon as FAST finishes)"]}

def build():
    g = StateGraph(S)
    g.add_node("fast", fast)
    g.add_node("slow", slow)
    g.add_node("after_fast", after_fast)

    # Parallel fan-out from START
    g.add_edge(START, "fast")
    g.add_edge(START, "slow")

    # Downstream node depends ONLY on 'fast'
    g.add_edge("fast", "after_fast")

    # Finish graph (slow has its own terminal path)
    g.add_edge("after_fast", END)
    g.add_edge("slow", END)

    return g.compile()

if __name__ == "__main__":
    app = build()
    final = asyncio.run(app.ainvoke({"log": []}))
    print("\n".join(final["log"]))

Error Message and Stack Trace (if applicable)


Description

Expected behavior

In the code shown above, because after_fast only depends on fast, it should be able to run immediately after fast finishes (~0.10s), without waiting for the unrelated slow branch (~1.20s). This expectation aligns with how nodes/edges are described in LangGraph’s Graph API (edges define dependencies; after_fast has no dependency on slow).
LangChain Docs

Actual behavior

after_fast appears to be delayed until the end of the superstep that also contains slow. In practice this means after_fast only runs after slow completes, causing end-to-end latency to be ~1.2s instead of ~0.1s.

Sample output (typical):

**0.104s FAST done
1.307s SLOW done
1.308s AFTER_FAST ran (should run as soon as FAST finishes)**

So even though after_fast has no edge from slow, it doesn’t execute until the slow sibling completes.

Related issue: https://github.com/langchain-ai/langgraph/discussions/3636?utm_source=chatgpt.com

System Info

  • OS: macOS 14.6
  • Python: 3.12.5
  • LangGraph: 1.0.0 (clean venv, installed via pip install langgraph==1.0.0)
  • No LangSmith / no custom checkpointing
Originally created by @atulrawat on GitHub (Oct 21, 2025). Originally assigned to: @casparb on GitHub. ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/). - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue. ### Example Code ```python import asyncio, time from typing_extensions import Annotated from typing import TypedDict from langgraph.graph import StateGraph, START, END from operator import add class S(TypedDict): log: Annotated[list[str], add] def now(): return f"{time.perf_counter():.3f}s" async def fast(_s: S) -> dict: await asyncio.sleep(0.10) # ~100 ms return {"log": [f"{now()} FAST done"]} async def slow(_s: S) -> dict: await asyncio.sleep(1.20) # ~1.2 s return {"log": [f"{now()} SLOW done"]} async def after_fast(_s: S) -> dict: return {"log": [f"{now()} AFTER_FAST ran (should run as soon as FAST finishes)"]} def build(): g = StateGraph(S) g.add_node("fast", fast) g.add_node("slow", slow) g.add_node("after_fast", after_fast) # Parallel fan-out from START g.add_edge(START, "fast") g.add_edge(START, "slow") # Downstream node depends ONLY on 'fast' g.add_edge("fast", "after_fast") # Finish graph (slow has its own terminal path) g.add_edge("after_fast", END) g.add_edge("slow", END) return g.compile() if __name__ == "__main__": app = build() final = asyncio.run(app.ainvoke({"log": []})) print("\n".join(final["log"])) ``` ### Error Message and Stack Trace (if applicable) ```shell ``` ### Description **Expected behavior** In the code shown above, because **after_fast** only depends on fast, it should be able to run **immediately after** fast finishes (~0.10s), **without** waiting for the unrelated slow branch (~1.20s). This expectation aligns with how nodes/edges are described in LangGraph’s Graph API (edges define dependencies; after_fast has no dependency on slow). [LangChain Docs](https://docs.langchain.com/oss/python/langgraph/use-graph-api?utm_source=chatgpt.com) **Actual behavior** **after_fast** appears to be delayed until the end of the superstep that also contains slow. In practice this means after_fast only runs after slow completes, causing end-to-end latency to be ~1.2s instead of ~0.1s. Sample output (typical): ``` **0.104s FAST done 1.307s SLOW done 1.308s AFTER_FAST ran (should run as soon as FAST finishes)** ``` So even though **after_fast** has no edge from slow, it doesn’t execute until the slow sibling completes. Related issue: https://github.com/langchain-ai/langgraph/discussions/3636?utm_source=chatgpt.com ### System Info - OS: macOS 14.6 - Python: 3.12.5 - LangGraph: 1.0.0 (clean venv, installed via pip install langgraph==1.0.0) - No LangSmith / no custom checkpointing
yindo added the bugpending labels 2026-02-20 17:42:44 -05:00
yindo closed this issue 2026-02-20 17:42:44 -05:00
Author
Owner

@dayeguilaiye commented on GitHub (Oct 24, 2025):

Found this problem too

@dayeguilaiye commented on GitHub (Oct 24, 2025): Found this problem too
Author
Owner

@roger-schaer commented on GitHub (Nov 5, 2025):

Also experiencing this, any news regarding a fix for this issue?

@roger-schaer commented on GitHub (Nov 5, 2025): Also experiencing this, any news regarding a fix for this issue?
Author
Owner

@casparb commented on GitHub (Nov 7, 2025):

Hi @atulrawat, good question. cc @dayeguilaiye @roger-schaer

LangGraph uses a super-step execution model, not a traditional DAG-style execution model. This is a critical distinction.

In each super-step, LangGraph:

  • Executes all nodes whose incoming edges were triggered in the previous super-step
  • Waits for all nodes in that super-step to complete before moving to the next super-step
  • Then determines which nodes should run in the next super-step

In your graph:

  • Super-step 1: fast and slow both run in parallel (both have edges from START)
  • Super-step 2: after_fast runs only after both fast and slow complete

This is why after_fast waits for slow to finish, even though there's no edge between them. They're both in the same super-step, and LangGraph waits for the entire super-step to complete before moving forward.

If you want (fast, after_fast) to execute in parallel with (slow), but you don't want after_fast to wait for slow, then you can bundle fast and after_fast in a subgraph:

def build():
    subgraph = StateGraph(S)
    subgraph.add_node("fast", fast)
    subgraph.add_node("after_fast", after_fast)
    subgraph.add_edge(START, "fast")
    subgraph.add_edge("fast", "after_fast")

    g = StateGraph(S)
    g.add_node("fast", subgraph.compile())
    g.add_node("slow", slow)

    g.add_edge(START, "fast")
    g.add_edge(START, "slow")

    g.add_edge("fast", END)
    g.add_edge("slow", END)

    return g.compile()

This produces this graph:

Image

And we get:

154525.124s FAST done
154525.125s AFTER_FAST ran (should run as soon as FAST finishes)
154526.224s SLOW done

The Thinking in LangGraph docs might be helpful, and I would also advise to checkout the Graphs section of the Graph API overview page for more info on super-steps.

@casparb commented on GitHub (Nov 7, 2025): Hi @atulrawat, good question. cc @dayeguilaiye @roger-schaer LangGraph uses a super-step execution model, not a traditional DAG-style execution model. This is a critical distinction. In each super-step, LangGraph: - Executes all nodes whose incoming edges were triggered in the previous super-step - Waits for all nodes in that super-step to complete before moving to the next super-step - Then determines which nodes should run in the next super-step In your graph: - Super-step 1: `fast` and `slow` both run in parallel (both have edges from `START`) - Super-step 2: `after_fast` runs only after both `fast` and `slow` complete This is why `after_fast` waits for slow to finish, even though there's no edge between them. They're both in the same super-step, and LangGraph waits for the entire super-step to complete before moving forward. If you want (fast, after_fast) to execute in parallel with (slow), but you don't want after_fast to wait for slow, then you can bundle `fast` and `after_fast` in a subgraph: ```python def build(): subgraph = StateGraph(S) subgraph.add_node("fast", fast) subgraph.add_node("after_fast", after_fast) subgraph.add_edge(START, "fast") subgraph.add_edge("fast", "after_fast") g = StateGraph(S) g.add_node("fast", subgraph.compile()) g.add_node("slow", slow) g.add_edge(START, "fast") g.add_edge(START, "slow") g.add_edge("fast", END) g.add_edge("slow", END) return g.compile() ``` This produces this graph: <img height="300" alt="Image" src="https://github.com/user-attachments/assets/6d8bf257-3a3c-4532-bfac-702b2cd155fa" /> And we get: ``` 154525.124s FAST done 154525.125s AFTER_FAST ran (should run as soon as FAST finishes) 154526.224s SLOW done ``` The [Thinking in LangGraph](https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph) docs might be helpful, and I would also advise to checkout the [Graphs](https://docs.langchain.com/oss/python/langgraph/graph-api) section of the Graph API overview page for more info on super-steps.
Author
Owner

@roger-schaer commented on GitHub (Nov 10, 2025):

Thanks a lot for the explanation @casparb, that clears things up!

@roger-schaer commented on GitHub (Nov 10, 2025): Thanks a lot for the explanation @casparb, that clears things up!
Author
Owner

@atulrawat commented on GitHub (Nov 10, 2025):

Thanks @casparb for the detailed explanation!

In each super-step, LangGraph:

Executes all nodes whose incoming edges were triggered in the previous super-step
Waits for all nodes in that super-step to complete before moving to the next super-step
Then determines which nodes should run in the next super-step

Bundling fast and after_fast in a subgraph feels more like a workaround to me and super-step execution model vs DAG-style execution model is more of a low level implementation detail here. Would LangGraph consider supporting this sort of a feature via the framework itself in future?

@atulrawat commented on GitHub (Nov 10, 2025): Thanks @casparb for the detailed explanation! > In each super-step, LangGraph: > > Executes all nodes whose incoming edges were triggered in the previous super-step > Waits for all nodes in that super-step to complete before moving to the next super-step > Then determines which nodes should run in the next super-step Bundling **fast** and **after_fast** in a subgraph feels more like a workaround to me and super-step execution model vs DAG-style execution model is more of a low level implementation detail here. Would LangGraph consider supporting this sort of a feature via the framework itself in future?
Author
Owner

@casparb commented on GitHub (Nov 12, 2025):

@atulrawat LangGraph is a low-level framework, learning the super-step execution model (at least from a high-level, as it is explained in the docs I linked above) is key to knowing how to build with LangGraph. In regards to your question about supporting this feature - we do support this behavior with subgraphs, and there aren't plans to change this architecture. Subgraphs are a core part of the framework, not a workaround by any means, so don't be afraid to use the solution I gave above!

To think about it differently, you and I both agree that fast and after_fast should execute together in sequence and shouldn't be blocked by what other nodes are doing. In LangGraph, when we want this sort of isolated behavior, we use a subgraph. Subgraphs bundle many nodes together into a single graph that we can then add to our top-level graph, to get executed just as a normal node would.

A practical example would be if you had an email manager agent and one isolated action it performs is sending an email. This action can be broken down into four nodes: do research, draft email, human review, send email. I want these nodes to execute one-after-the-other, and not be delayed by other parallel tasks my email manager agent may be performing (e.g. filtering emails for spam). We can achieve this isolation by bundling these four nodes into a subgraph called send_email, then adding that subgraph as a regular node in our main graph:

g.add_node("send_email", compiled_send_email_subgraph)

Hopefully this makes sense.

@casparb commented on GitHub (Nov 12, 2025): @atulrawat LangGraph is a low-level framework, learning the super-step execution model (at least from a high-level, as it is explained in the docs I linked above) is key to knowing how to build with LangGraph. In regards to your question about supporting this feature - we do support this behavior with subgraphs, and there aren't plans to change this architecture. Subgraphs are a core part of the framework, not a workaround by any means, so don't be afraid to use the solution I gave above! To think about it differently, you and I both agree that `fast` and `after_fast` should execute together in sequence and shouldn't be blocked by what other nodes are doing. In LangGraph, when we want this sort of isolated behavior, we use a subgraph. Subgraphs bundle many nodes together into a single graph that we can then add to our top-level graph, to get executed just as a normal node would. A practical example would be if you had an email manager agent and one isolated action it performs is sending an email. This action can be broken down into four nodes: do research, draft email, human review, send email. I want these nodes to execute one-after-the-other, and not be delayed by other parallel tasks my email manager agent may be performing (e.g. filtering emails for spam). We can achieve this isolation by bundling these four nodes into a subgraph called send_email, then adding that subgraph as a regular node in our main graph: ```python g.add_node("send_email", compiled_send_email_subgraph) ``` Hopefully this makes sense.
Author
Owner

@atulrawat commented on GitHub (Nov 13, 2025):

Thanks again for this detailed explanation and taking time to explain the underlying ideas here. I was thinking of subgraphs as simply a nodes organizing mechanism and hence the confusion. But your above explanation clears things up!

@atulrawat commented on GitHub (Nov 13, 2025): Thanks again for this detailed explanation and taking time to explain the underlying ideas here. I was thinking of subgraphs as simply a nodes organizing mechanism and hence the confusion. But your above explanation clears things up!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#1013