Skip to content

perf: Canvas slows down progressively after repeated Playground runs (unbounded message history fetch) #13460

@YeonghyeonKO

Description

@YeonghyeonKO

Description

After running a flow with should_store_message=true (the default) 50–100 times, the flow canvas becomes noticeably slower with each run. The only recovery is a full page reload — which itself re-triggers the same problem because the messages are immediately re-fetched from the database.

Root Cause

Two compounding issues:

1. GET /monitor/messages has no row limit

The endpoint returns all stored messages for a flow with no LIMIT clause. After 100 runs, a typical chat flow (user message + AI reply) produces 200+ rows. Every refetch deserializes and delivers all of them.

# monitor.py — no limit applied
messages = await session.exec(stmt)
return [MessageResponse.model_validate(d, from_attributes=True) for d in messages]

2. useChatHistory fetches even when the Playground is hidden

SimpleSidebar keeps its content mounted using visibility: hidden rather than unmounting it. Because of this, useChatHistory and its useGetMessagesQuery call remain active even when the user has closed the Playground panel.

// use-chat-history.ts — no enabled guard
const { data: queryData } = useGetMessagesQuery(messageQueryParams);

React Query's default behavior refetches stale data on window focus and component subscription. With 200+ messages returning on each refetch, setMessages(200+ items) triggers a full Zustand re-render cascade that includes canvas components.

Steps to Reproduce

  1. Open any flow with a Chat Input component (should_store_message=true)
  2. Open the Playground and send 50–100 messages
  3. Observe the canvas becoming progressively less responsive after each run
  4. Close the Playground panel — canvas remains slow
  5. Page reload temporarily restores speed, but slows again after a few more runs

Expected Behavior

Canvas responsiveness should remain constant regardless of message history length or Playground state.

Proposed Fix

Backend — add optional limit param (default 20) and offset param to GET /monitor/messages:

@router.get("/messages")
async def get_messages(
    ...
    limit: Annotated[int | None, Query(ge=1)] = 20,
    offset: Annotated[int | None, Query(ge=0)] = None,
) -> list[MessageResponse]:
    ...
    if order_by:
        order_col = getattr(MessageTable, order_by).desc()  # newest first
        stmt = stmt.order_by(order_col)
    if limit:
        stmt = stmt.limit(limit)
    if offset:
        stmt = stmt.offset(offset)

Frontend — gate useChatHistory fetch on playground visibility, pass the limit, and support scroll-up pagination:

const isPlaygroundOpen = usePlaygroundStore((state) => state.isOpen);

const { data: queryData } = useGetMessagesQuery(
  { id: currentFlowId, mode: "union", params: { limit: 20 } },
  { enabled: isPlaygroundOpen }
);

// loadMore: fetches older messages with offset, prepends to cache
const loadMore = useCallback(async () => {
  const response = await api.get(getURL("MESSAGES"), {
    params: { flow_id: currentFlowId, limit: 20, offset: offset + 20 },
  });
  // prepend to session cache
}, [...]);

A LoadMoreTrigger component using IntersectionObserver automatically calls loadMore when the user scrolls to the top of the chat.

Environment

  • Langflow 1.9.x
  • Reproducible with both SQLite and PostgreSQL backends
  • Confirmed via Playwright automation (repeated Chat Input runs)

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingjiraThis issue has been logged in Jira for fix by the engineering team.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions