Skip to content

Excessive API polling when features are in progress #630

@stefandevo

Description

@stefandevo

Problem

When features are in progress with auto-mode running, the UI makes excessive HTTP requests to multiple endpoints nearly every second:

POST /api/worktree/list 200
POST /api/worktree/list 200
OPTIONS /api/features/get 204
POST /api/features/get 200
OPTIONS /api/features/agent-output 204
POST /api/features/agent-output 200
OPTIONS /api/worktree/list 204
OPTIONS /api/worktree/init-script 204
GET /api/worktree/init-script 200

Root Cause Analysis

The excessive requests are caused by multiple overlapping refresh mechanisms:

1. Agent Info Panel Polling (agent-info-panel.tsx:78-86)

Each feature card in progress has an AgentInfoPanel that polls both:

  • useFeature every 3 seconds
  • useAgentOutput every 3 seconds

With N features in progress, this creates 2N API calls every 3 seconds.

2. Auto-Mode Progress Event Invalidation (use-query-invalidation.ts:80-84)

if (event.type === 'auto_mode_progress' && 'featureId' in event) {
  queryClient.invalidateQueries({
    queryKey: queryKeys.features.agentOutput(projectPath, event.featureId),
  });
}

When auto-mode is running, auto_mode_progress events are emitted frequently (likely with each agent output chunk). Each event invalidates the agentOutput query, triggering an immediate refetch - in addition to the 3-second polling.

3. Worktree Panel Polling (worktree-panel.tsx:189-199)

The worktree panel polls /api/worktree/list every 5 seconds via setInterval.

4. React StrictMode (dev only)

StrictMode double-invokes effects in development, potentially doubling some requests on component mount.

Proposed Solutions

Option 1: Debounce progress event invalidations (Recommended)

Instead of invalidating immediately on every auto_mode_progress event, debounce the invalidation:

// use-query-invalidation.ts
import { useMemo } from 'react';
import debounce from 'lodash.debounce';

// Debounce agent output invalidation to avoid excessive refetches
const debouncedInvalidateAgentOutput = useMemo(
  () => debounce((featureId: string) => {
    queryClient.invalidateQueries({
      queryKey: queryKeys.features.agentOutput(projectPath, featureId),
    });
  }, 1000), // 1 second debounce
  [queryClient, projectPath]
);

// Then in the event handler:
if (event.type === 'auto_mode_progress' && 'featureId' in event) {
  debouncedInvalidateAgentOutput(event.featureId);
}

Option 2: Disable polling when receiving events

If we're receiving WebSocket events for a feature, we don't need polling since the events drive invalidation. The AgentInfoPanel could detect this:

// Disable polling if we recently received a progress event
const shouldPoll = (isCurrentAutoTask || feature.status === 'in_progress') && !recentlyReceivedEvent;

Option 3: Remove init-script polling trigger

The useWorktreeInitScript hook doesn't need to refetch when worktrees are polled. It should only fetch once on mount and when explicitly invalidated (after save/delete mutations).

Option 4: Batch invalidations

Collect all invalidations during a frame and batch them into a single update.

Impact

  • Reduced server load
  • Better client performance (fewer React re-renders)
  • Reduced network traffic
  • Improved battery life on laptops/mobile

Files to Modify

  • apps/ui/src/hooks/use-query-invalidation.ts - Debounce progress event invalidations
  • apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx - Conditionally disable polling
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx - Consider if 5s polling is necessary

Metadata

Metadata

Assignees

Labels

BugSomething isn't working

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions