# 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:
- 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:
- 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:
- 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
No comments yet.