Back to Home

Next.js Integration with 1C Bitrix: Asynchronous Lead Transfer

Practical guide to asynchronous integration of Next.js 16 with 1C Bitrix. Using after() for background lead processing, setting up retry mechanisms and PostgreSQL schema. Solving timeout and duplicate issues.

Next.js and 1C Bitrix: how to send leads without queues and workers
Advertisement 728x90

# Asynchronous Lead Transfer from Next.js to 1C-Bitrix: A Queue- and Worker-Free Pattern

Synchronous lead submission to CRM via REST API causes critical issues: the user waits for a response until the external request completes, and the instability of the 1C-Bitrix API leads to timeouts and errors. We propose a solution on Next.js 16 that eliminates queues and background processes. The implementation saves the lead to a local PostgreSQL database, instantly responds to the user, and handles integration with Bitrix via after() — a mechanism for background processing after sending the HTTP response.

Queue-Free Architecture: Three Layers of Responsibility

Key principle — separating operations into synchronous and asynchronous parts. Upon receiving the form:

  • Data validation via zod
  • Save to PostgreSQL with bitrix_id=NULL
  • Instant 200 OK response
  • Background send to Bitrix via after()

This guarantees:

Google AdInline article slot
  • Independence from Bitrix REST API stability
  • No blocking of the user interface
  • Ability to manually process undelivered leads via SELECT * FROM leads WHERE bitrix_id IS NULL

The approach's key feature is ditching Redis/BullMQ. All operations fit into a Next.js API route, using the built-in after() for post-response processing. This reduces infrastructure complexity and simplifies deployment on VPS via systemd/nginx.

after() in Next.js 16: Technical Implementation

The after() method solves the "fire-and-forget" problem in serverless environments. Unlike Promise.resolve().then(), it ensures task execution even after sending the response, without terminating the process until the operation completes. Form processing example:

export async function POST(request: Request) {
  // ... validatsiya and antidubl
  
  const [lead] = await pgQuery(...);

  after(async () => {
    try {
      const bitrixId = await sendToBitrix24(payload);
      await pgQuery(
        `UPDATE leads SET bitrix_id = $1 WHERE id = $2`,
        [bitrixId, lead.id]
      );
    } catch (error) {
      console.error("[Leads API] Error otpravki in Bitriks:", error);
    }
  });

  return Response.json({ ok: true, id: lead.id });
}

Key nuances:

Google AdInline article slot
  • Anti-duplication implemented via a 10-minute window in PostgreSQL, not through Bitrix (CRM accepts duplicates without warning)
  • bitrix_id stored as nullable text — reflects the asynchronous nature of the operation
  • Integration errors logged to journalctl, without affecting the user response

Reliable HTTP Client for 1C-Bitrix

Critical parameters for stable work with an unstable API:

  • MAX_ATTEMPTS = 2 — balance between reliability and load
  • REQUEST_TIMEOUT_MS = 8000 — above Bitrix p95 latency (400-700 ms)
  • RETRY_DELAY_MS = 1500 — linear backoff without exponential complexity

Client implementation includes:

async function bitrixRequest(method: string, payload: Record<string, unknown>) {
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);

    try {
      const response = await fetch(url, {
        signal: controller.signal,
        // ...
      });

      const text = await response.text();
      const json = text ? JSON.parse(text) : {};

      if (!response.ok || json.error) {
        throw new Error(json.error_description || `HTTP ${response.status}`);
      }

      return json;
    } catch (error) {
      if (attempt >= MAX_ATTEMPTS) throw error;
      await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
    } finally {
      clearTimeout(timer);
    }
  }
}

Three key points:

Google AdInline article slot
  • Manual parsing via response.text() instead of response.json() — bypasses Bitrix errors with empty responses
  • Handling two types of errors: HTTP statuses and internal CRM errors
  • Guaranteed timer cleanup in finally — prevents memory leaks

Authentication Features and Data Schema

For integration, incoming webhook chosen over OAuth:

  • No dependency on Marketplace and approvals
  • Simple management of the BITRIX_WEBHOOK_URL env variable
  • Scopes configured in Bitrix admin panel

Risk of token leakage in URL minimized by rule: logs and code show only the API method, not the full URL.

leads table schema:

CREATE TABLE leads (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,
  phone text NOT NULL,
  source text,
  bitrix_id text,
  created_at timestamptz NOT NULL DEFAULT now()
);

Why bitrix_id text, not integer?

  • Bitrix returns ID as string in JSON
  • No need for type conversion
  • UUID in PostgreSQL ensures idempotency

Key Points

  • Single source of truth — local DB: lead saved before contacting Bitrix, ensuring data even on CRM failure
  • Post-response processing: after() replaces queues, eliminating infrastructure complexity
  • Error handling: retries with timeouts and journalctl logging ensure observability
  • DB-level anti-duplication: 10-minute window by phone prevents duplicates regardless of CRM
  • Secure authentication: webhook requires strict URL logging control

This pattern suits projects with up to 50 requests per minute. For high-load systems, add monitoring via SELECT COUNT(*) FROM leads WHERE bitrix_id IS NULL and automatic retries via cron. In the current implementation, manual error handling is preferable to automatic retries — it reduces the risk of lead duplication during temporary failures.

— Editorial Team

Advertisement 728x90

Read Next