# How OpenHiggsfield Scaled Next.js Server Actions: Request Coalescing, 38 AI Models, and Zero Queue Locks

> Source: <https://dev.to/niraj_matere/nextjs-server-action-serialization-high-throughput-request-coalescing-declarative-multi-model-1mg6>
> Published: 2026-09-18 04:36:49+00:00

When building production-grade Generative AI platforms, engineering teams face two core architectural hurdles: **network execution bottlenecks** caused by Next.js Server Action concurrency semantics, and **schema fragmentation** across dozens of heterogeneous AI provider APIs (e.g., Kling, ByteDance Seedance, Black Forest Labs Flux, Minimax).

In this deep dive, we will analyze the technical architecture of [`wide-trace/open-higgsfield`](https://github.com/wide-trace/open-higgsfield)—a high-performance, open-source studio UI designed for multi-model video and image generation. We will examine how to bypass Next.js Server Action client locks through **client-side request coalescing**, and how to build a **declarative multi-model catalog translation layer** that unifies dynamic parameter validation and multi-modal platform routing.

Next.js App Router Server Actions offer an elegant mental model for client-to-server mutations. However, their underlying runtime semantics introduce a major bottleneck for real-time applications: **Next.js serializes Server Action dispatches per client connection**.

In a generative studio, multiple long-running asynchronous inference tasks run concurrently. A naive implementation attaches a `setInterval` hook to each active generation task, issuing an isolated Server Action poll (`getGenerationStatus(requestId)`) every few seconds.

```
[ Client Execution Timeline - Naive Approach ]

Task A Poll ────► | Server Action 1 (In Flight) | ────────────────────────► Done
Task B Poll ────────► [ QUEUED in React Action Queue ] ──► | Server Action 2 | ──► Done
Task C Poll ────────────► [ QUEUED behind A & B ] ─────────────────────────────► Stalled
```

Because Next.js processes Server Actions sequentially per client, firing isolated actions for N jobs causes request queueing on the client. If three video generations are polling every 4 seconds, and a user clicks "Submit Generation", the submission Server Action is placed at the back of the queue. The UI stalls, user input freezes, and the application exhibits high latency despite the backend platform remaining idle.

To bypass client-side Action serialization, `open-higgsfield` implements a **single-flight fan-out polling architecture**. Instead of permitting independent timers to dispatch isolated Server Actions, the client pools all pending generation IDs into a central state machine (`src/generation/poll.ts`) and dispatches a single batched Server Action per interval.

```
+-----------------------------------------------------------------------------------+
|                               CLIENT-SIDE RUNTIME                                 |
|                                                                                   |
|  +-------------------+  watchRequest(id_1)  +----------------------------------+  |
|  | Active Job UI #1  | ------------------► |                                  |  |
|  +-------------------+                     |   Coalescing Engine (poll.ts)    |  |
|  +-------------------+  watchRequest(id_2)  |   - waiting: Map<id, Waiter>     |  |
|  | Active Job UI #2  | ------------------► |   - Single timer tick (4000ms)   |  |
|  +-------------------+                     +----------------------------------+  |
+-------------------------------------------------------------|---------------------+
                                                              |
                                           Single Server Action Dispatch
                                           getGenerationStatuses([id_1, id_2])
                                                              |
+-------------------------------------------------------------▼---------------------+
|                               SERVER-SIDE RUNTIME                                 |
|                                                                                   |
|  +-----------------------------------------------------------------------------+  |
|  | Server Action Handlers (actions.ts)                                         |  |
|  |                                                                             |  |
|  |   Promise.all([                                                             |  |
|  |     client.status("id_1"),  <-- Concurrent Node.js Fetch Calls              |  |
|  |     client.status("id_2"),                                                  |  |
|  |   ])                                                                        |  |
|  +-----------------------------------------------------------------------------+  |
+-----------------------------------------------------------------------------------+
```

*(Architecture flow mapped using [Documentor Pro](https://documentor-pro.com) — automated dependency graph mapping and codebase visualization)*

`src/generation/poll.ts`)
The client-side engine uses an in-memory `Map<string, Waiter>` to register waiting tasks. Promises are held open across polling ticks and resolved when the server reports a terminal state (`completed`, `failed`, `nsfw`, `canceled`).

``` js
// src/generation/poll.ts

const TERMINAL = new Set(["completed", "failed", "nsfw", "canceled"]);
export const POLL_INTERVAL_MS = 4000;
export const POLL_DEADLINE_MS = 10 * 60_000;
const MAX_MISSES = 3;

type Waiter = {
  deadline: number;
  resolve: (status: GenerationStatus) => void;
  reject: (reason: Error) => void;
};

const waiting = new Map<string, Waiter>();
const inflight = new Map<string, Promise<GenerationStatus>>();
let timer: ReturnType<typeof setTimeout> | null = null;
let polling = false;
let misses = 0;

export function watchRequest(
  requestId: string,
  opts?: { deadline?: number },
): Promise<GenerationStatus> {
  const existing = inflight.get(requestId);
  if (existing) return existing;

  const promise = new Promise<GenerationStatus>((resolve, reject) => {
    waiting.set(requestId, {
      deadline: opts?.deadline ?? Date.now() + POLL_DEADLINE_MS,
      resolve: (status) => {
        inflight.delete(requestId);
        resolve(status);
      },
      reject: (reason) => {
        inflight.delete(requestId);
        reject(reason);
      },
    });
    schedule();
  });

  inflight.set(requestId, promise);
  return promise;
}
```

`inflight` map caching prevents duplicate polling triggers for the same `requestId`.` schedule()` ensures that only a single `setTimeout` loop runs globally across the entire UI tree, regardless of how many components register request watches.`misses` counter tracks consecutive batch request failures:

```
// src/generation/poll.ts

async function round(): Promise<void> {
  timer = null;
  polling = true;
  try {
    // Single batched Server Action call carrying array of active request IDs
    const results = await getGenerationStatuses({ requestIds: [...waiting.keys()] });
    misses = 0;
    for (const result of results) deliver(result);
    sweep();
  } catch (caught) {
    // Tolerates up to MAX_MISSES back-to-back failed network rounds
    if (++misses < MAX_MISSES) return;
    settleAll(caught instanceof Error ? caught : new Error(String(caught)));
  } finally {
    polling = false;
    schedule();
  }
}
```

On the server side, `getGenerationStatuses` receives the array of pooled request IDs and delegates execution to `Promise.all`. This shifts fan-out concurrency to Node.js async I/O routines on the server runtime, executing downstream HTTP queries concurrently without hitting Next.js client-side locks.

```
// src/generation/actions.ts

export async function getGenerationStatuses(data: unknown): Promise<StatusResult[]> {
  const requestIds = parseRequestIds(data);
  const client = createPlatformClient(await readCredentials());

  return Promise.all(
    requestIds.map(async (requestId): Promise<StatusResult> => {
      try {
        return { requestId, status: await client.status(requestId) };
      } catch (caught) {
        // Tagged union return pattern preserves sibling results if one request fails
        return { requestId, error: caught instanceof Error ? caught.message : String(caught) };
      }
    }),
  );
}
```

`StatusResult`
To prevent a failure in status polling for one model execution from rejecting the entire batch, the platform uses a Discriminated Union pattern (`StatusResult`):

```
// src/generation/platform.ts

export type StatusResult =
  | { requestId: string; status: GenerationStatus }
  | { requestId: string; error: string };
```

When delivering results on the client, errors are handled at the item level:

```
// src/generation/poll.ts

function deliver(result: StatusResult): void {
  const waiter = waiting.get(result.requestId);
  if (!waiter) return;

  if ("error" in result) {
    waiting.delete(result.requestId);
    waiter.reject(new Error(result.error)); // Rejects specific job promise
    return;
  }

  if (!TERMINAL.has(result.status.status)) return; // Keep polling if non-terminal
  waiting.delete(result.requestId);
  waiter.resolve(result.status); // Resolves job promise on completion
}
```

This guarantees **fault isolation**: if job $A$ throws an upstream 500 error, job $B$'s polling promise stays intact and continues to resolve seamlessly.

Managing 30+ generative image and video models (e.g., Soul Cinema, Kling 3.0, ByteDance Seedance, Black Forest Labs Flux) presents significant engineering complexity:

To solve this, `open-higgsfield` adopts an intermediate metadata pattern using a **Declarative Multi-Model Catalog Pipeline**.

```
                   [ UI Layer (Composer / Controls) ]
                                   │
                                   ▼
                   [ Model Metadata Schema (types.ts) ]
                    - Settings schema (enum/range/bool)
                    - Asset slot constraints
                                   │
                                   ▼
                   [ Intermediate Representation ]
                   GenerationPlane {
                     model: "kling-3-pro",
                     prompt: { text: "..." },
                     media: { start: [...], end: [...] },
                     settings: { duration: 5, aspectRatio: "16:9" }
                   }
                                   │
                                   ▼
                   [ Schema Validator (parseSettings.ts) ]
                    - Clamps bounds
                    - Falls back to defaults
                                   │
                                   ▼
                   [ Platform Mapper (to-platform.ts) ]
                    - Dynamic Path Matcher (mapByPaths)
                    - Custom Mappers (mapKling3, mapSeedance)
                                   │
                                   ▼
                  [ Upstream HTTP Payload Generation ]
                  { path: "kling-video/v3.0/pro/image-to-video", body: { ... } }
```

`GenerationPlane`)
Instead of constructing provider-specific payloads directly in client state, the UI interacts exclusively with a normalized intermediate schema called `GenerationPlane`:

```
// src/generation/catalog/types.ts

export type Surface = "image" | "video";
export type MediaRole = "start" | "end" | "reference" | "video" | "audio";

export type MediaItem = {
  id: string;
  url: string;
  role: MediaRole;
};

export type SettingField =
  | { type: "enum"; values: readonly string[]; default: string }
  | { type: "range"; min: number; max: number; default: number; step?: number }
  | { type: "boolean"; default: boolean };

export type ModelEntry = {
  id: string;
  surface: Surface;
  label: string;
  roles: Partial<Record<MediaRole, number>>; // Max items per slot role
  settings: Record<string, SettingField>;
  paths?: PlatformPaths;
};

export type GenerationPlane = {
  model: string;
  prompt: { text: string };
  media: Partial<Record<MediaRole, MediaItem[]>>;
  settings: Record<string, unknown>;
};
```

When a request is submitted, raw values sent from the client pass through `parseSettings`. This module evaluates input configurations against the catalog model schema, enforcing boundary constraints and fallback values before payload transformation occurs:

``` python
// src/generation/catalog/parse-settings.ts

import type { ModelEntry } from "./types";

export function parseSettings(
  model: ModelEntry,
  raw: Record<string, unknown>,
): Record<string, unknown> {
  const out: Record<string, unknown> = {};

  for (const [key, field] of Object.entries(model.settings)) {
    const value = raw[key];

    if (field.type === "enum") {
      const picked = typeof value === "string" ? value : field.default;
      if (!field.values.includes(picked)) throw new Error(`Invalid ${key}`);
      out[key] = picked;
      continue;
    }

    if (field.type === "range") {
      const picked = typeof value === "number" ? value : field.default;
      if (picked < field.min || picked > field.max) throw new Error(`Invalid ${key}`);
      out[key] = picked;
      continue;
    }

    out[key] = typeof value === "boolean" ? value : field.default;
  }

  return out;
}
```

Model metadata is defined declaratively using functional builders (`videoModel`, `imageModel`, `t2v`), eliminating boilerplate while standardizing output signatures:

```
// src/generation/catalog/defaults.ts

export function t2v(path: string): PlatformPaths {
  if (!path.endsWith("/text-to-video")) return { text: path };
  return { text: path, image: path.replace(/\/text-to-video$/, "/image-to-video") };
}

export function videoModel(
  id: string,
  label: string,
  roles: Partial<Record<MediaRole, number>>,
  paths: PlatformPaths,
): ModelEntry {
  return {
    id,
    surface: "video",
    label,
    roles,
    settings: {
      aspectRatio: { type: "enum", values: ["16:9", "9:16", "1:1"], default: "16:9" },
      resolution: { type: "enum", values: ["720p", "1080p"], default: "720p" },
      duration: { type: "range", min: 4, max: 10, default: 5 },
    },
    paths,
  };
}
```

Defining a new model (e.g., Flux 3) requires only a single line of metadata configuration:

``` js
// src/generation/catalog/flux-3.ts

import { t2v, videoModel } from "./defaults";

export const flux3 = videoModel(
  "flux-3",
  "Flux 3",
  { start: 1 },
  t2v("blackforestlabs/flux-3/text-to-video"),
);
```

`to-platform.ts`)
The structural bridge between the abstract `GenerationPlane` and destination platform endpoints is managed by `toPlatform()`. This layer implements a **Strategy Pattern** that routes models through either generic structural mappers (`mapByPaths`) or dynamic procedural mappers (such as `mapKling3` and `mapSeedance`).

```
// src/generation/to-platform.ts

export function toPlatform(plane: GenerationPlane): Mapped {
  const model = getModel(plane.model);
  const map = MAP[model.id] ?? (model.paths ? (next) => mapByPaths(next, model.paths!) : undefined);
  if (!map) throw new Error(`No platform map for ${plane.model}`);
  return map(plane);
}
```

`mapByPaths`):
For models adhering to common input patterns, `mapByPaths` automatically derives path routes and body schemas based on attached frame media:

```
// src/generation/to-platform.ts

function mapByPaths(plane: GenerationPlane, spec: PlatformPaths): Mapped {
  const start = urls(plane, "start")[0];
  const end = urls(plane, "end")[0];
  const refs = urls(plane, "reference");
  const videos = urls(plane, "video");

  const body: Record<string, unknown> = {
    prompt: plane.prompt.text,
    ...(plane.settings.aspectRatio ? { aspect_ratio: plane.settings.aspectRatio } : {}),
    ...(plane.settings.resolution ? { resolution: plane.settings.resolution } : {}),
    ...(typeof plane.settings.duration === "number" ? { duration: plane.settings.duration } : {}),
  };

  if (spec.firstLast && (start || end)) {
    return {
      path: spec.firstLast,
      body: {
        ...body,
        ...(start ? { first_frame_url: start } : {}),
        ...(end ? { last_frame_url: end } : {}),
      },
    };
  }

  if (spec.image && start) {
    return {
      path: spec.image,
      body: { ...body, image_url: start, ...(end ? { last_image_url: end } : {}) },
    };
  }

  if (spec.text) {
    return { path: spec.text, body: refs.length ? { ...body, image_urls: refs } : body };
  }

  throw new Error("Model has no platform path");
}
```

When providers require bespoke properties (e.g., conditional path switches for Image-to-Video vs Text-to-Video combined with sound flags), specialized map functions are used:

```
// src/generation/to-platform.ts

function mapKling3(plane: GenerationPlane, prefix: string): Mapped {
  const start = urls(plane, "start")[0];
  const end = urls(plane, "end")[0];

  const body: Record<string, unknown> = {
    prompt: plane.prompt.text,
    sound: plane.settings.sound ? "on" : "off",
    duration: plane.settings.duration,
    cfg_scale: plane.settings.cfgScale,
    multi_shots: plane.settings.multiShots,
  };

  if (start) {
    body.image_url = start;
    if (end) body.last_image_url = end;
    return { path: `${prefix}/image-to-video`, body };
  }

  body.aspect_ratio = plane.settings.aspectRatio;
  return { path: `${prefix}/text-to-video`, body };
}
```

To highlight how these architectural layers interact, let's trace a user submission from UI event dispatch to status resolution.

```
[ UI Layer ] ──► User clicks "Generate" (Composer.tsx)
                      │
                      ▼
[ Action Layer ] ──► Calls submitGeneration(plane)
                          │  - Look up Model Schema
                          │  - Execute parseSettings(model, plane.settings)
                          │  - Invoke toPlatform(parsed) -> return { path, body }
                          ▼
[ Platform Layer ] ──► POST /kling-video/v3.0/pro/image-to-video
                            │  - Returns QueuedGeneration { requestId: "req_123" }
                            ▼
[ Client Poller ] ──► Component invokes watchRequest("req_123")
                            │  - Appends "req_123" to in-memory waiting Map
                            │  - Polling Engine wakes up on 4000ms tick
                            ▼
[ Batched Poll ] ──► Dispatch getGenerationStatuses({ requestIds: ["req_123", "req_456"] })
                            │  - Server executes Promise.all across platform APIs
                            │  - Returns StatusResult[] array
                            ▼
[ Resolution ] ──► deliver(result) detects status: "completed"
                            │  - Resolves watchRequest Promise
                            └─► UI renders generated video asset
```

The catalog engine drives both backend translation and client UI behaviors. The `Composer` component (`src/openhiggsfield/composer.tsx`) dynamically adapts its input surface based on metadata defined in the selected model entry:

```
// src/openhiggsfield/composer.tsx (excerpt)

export function Composer({ surface, model, ...props }: ComposerProps) {
  const settings = useSettings();
  const values = parseSettings(model, settings.byModel[model.id] ?? {});

  // Model settings inspection
  const native = countSetting(model);
  const counts = native ? native.counts : STUDIO_COUNTS;
  const batchValue = native ? Number(values[native.key]) || counts[0]! : batch;
  const settingKeys = Object.keys(model.settings).filter((key) => key !== native?.key);

  return (
    <div className="ohf-composer">
      {/* Dynamic render of controls governed entirely by model schema keys */}
      {settingKeys.map((key) => (
        <SettingPill
          key={key}
          model={model}
          settingKey={key}
          values={values}
          open={overlay === `setting:${key}`}
          onOpen={(trigger) => toggle(`setting:${key}`, trigger)}
        />
      ))}

      {/* Keyboard Shortcut & Execution trigger */}
      <button 
        disabled={prompt.text.trim().length === 0}
        onClick={onGenerate}
      >
        Generate
      </button>
    </div>
  );
}
```

This decoupled configuration model allows developers to integrate new backend AI providers without modifying component render logic or altering state handlers.

As full-stack AI platforms scale, maintaining complex runtime behavior—such as Server Action coalescing queues, schema mappers, dynamic UI generators, and API transformers—presents ongoing engineering challenges. Without clear system visibility, complex Next.js systems can suffer from architectural drift, opaque dependency hierarchies, and high onboarding costs for new engineers.

To prevent documentation drift and maintain structural clarity, modern development teams use visual dependency analysis tools.

**[Documentor Pro](https://documentor-pro.com)** automatically parses complex TypeScript codebases, maps server-to-client boundaries, and visually renders live architectural diagrams.

```
+-------------------------------------------------------------------------------+
|                             DOCUMENTOR PRO ENGINE                             |
|                                                                               |
|   +-----------------------+     Static AST     +--------------------------+   |
|   | Next.js App Source    | -----------------► | Dependency Analysis      |   |
|   | - Server Actions      |                    | - Import Graph Resolution|   |
|   | - Declarative Schemas |                    | - Dynamic Control Flow   |   |
|   +-----------------------+                    +--------------------------+   |
|                                                             │                 |
|                                                             ▼                 |
|                                                +--------------------------+   |
|                                                | Automated Architecture   |   |
|                                                | Mapping & Diagrams       |   |
|                                                +--------------------------+   |
+-------------------------------------------------------------------------------+
```

By integrating with your CI/CD workflow, Documentor Pro generates interactive system blueprints directly from source code, making it easy to audit complex async workflows and multi-model data pipelines.

👉 **Check out [Documentor Pro](https://documentor-pro.com)** to automatically map and document your services.

Building scalable, multi-model AI applications in Next.js requires working alongside the framework's concurrency defaults. By replacing unbatched execution loops with a **client-side coalescing engine**, you eliminate Server Action queue locks and maintain responsive user interfaces. Pair this with an intermediate **declarative catalog layer**, and your platform gains a flexible architecture capable of supporting dozens of AI provider models through standardized, type-safe data transformations.
