v1.2.0

Restrictions System

The restrictions system provides a comprehensive mechanism for managing request frequency, operation execution time, and adaptive delays.

Overview

The system consists of several components working together to prevent exceeding API limits and ensure stable operation.

┌─────────────────────────────────────────────┐
RestrictionManager│  (Coordinator of all types of restrictions) │
└─────┬──────────────┬──────────────┬─────────┘
      │              │              │
      ▼              ▼              ▼
┌──────────┐  ┌────────────┐  ┌─────────────┐
Rate     │  │ Operating  │  │ AdaptiveLimiter  │  │ Limiter    │  │ Delayer└──────────┘  └────────────┘  └─────────────┘

Interfaces and Types

ILimiter (Base Interface)

interface ILimiter {
  getTitle(): string
  setConfig(config: any): Promise<void>
  setLogger(logger: LoggerInterface): void
  getLogger(): LoggerInterface
  canProceed(requestId: string, method: string, params?: any): Promise<boolean>
  waitIfNeeded(requestId: string, method: string, params?: any): Promise<number>
  updateStats(requestId: string, method: string, data: any): Promise<void>
  reset(): Promise<void>
  getStats(): Record<string, any>
}

RestrictionParams

interface RestrictionParams {
  rateLimit?: RateLimitConfig      // Rate limiting settings
  operatingLimit?: OperatingLimitConfig  // Operation time limit settings
  adaptiveConfig?: AdaptiveConfig  // Adaptive delay settings
  maxRetries?: number              // Maximum number of request retries (default: 3). Except for `batch` requests
  retryDelay?: number              // Base delay between retries in ms (default: 1000)
  retryOnNetworkError?: boolean    // Retry on `NETWORK_ERROR` / `REQUEST_TIMEOUT` (HTTP 408) (default: true).
                                   // Set to `false` for non-idempotent calls — see "Non-idempotent calls" below.
  hardErrorCodes?: string[]        // Extra codes to throw immediately (merged with built-in hard list).
  softErrorCodes?: string[]        // Extra codes to return as AjaxResult with error (merged with built-in soft list).
}

System Components

1. RateLimiter (Rate Limiting)

Purpose: Implements the "Leaky Bucket" algorithm for limiting request frequency.

RateLimiter — Configuration

interface RateLimitConfig {
  burstLimit: number    // Bucket capacity (maximum number of simultaneous requests)
  drainRate: number     // Leak rate (requests per second)
  adaptiveEnabled: boolean  // Enable adaptive management
}

RateLimiter — Operation Principle

  1. Token-based system: Each request consumes one token
  2. Automatic replenishment: Tokens are replenished at the rate of drainRate
  3. Adaptive management:
  • With frequent errors, limits are automatically reduced by 20%
  • With stable operation, limits are gradually restored
  • Minimum values: drainRate = 0.5, burstLimit = 5

2. OperatingLimiter (Operation Time Limiting)

Purpose: Controls total operation execution time within a sliding window.

OperatingLimiter — Configuration

interface OperatingLimitConfig {
  windowMs: number      // Time period in milliseconds (default: 600000 = 10 minutes)
  limitMs: number       // Maximum execution time in milliseconds (default: 480000 = 480 seconds)
  heavyPercent: number  // Threshold for heavy request notifications (%)
}

OperatingLimiter — Operation Principle

  1. Sliding window: Tracks execution time over the last 10 minutes
  2. Method-specific tracking: Statistics are kept separately for each method
  3. Safety buffer: Calculations use limitMs - 5000 (5 second buffer)
  4. Blocking: When the limit is reached, blocks execution until statistics are reset

OperatingLimiter — Features

  • For batch requests, analyzes all nested methods
  • Automatic cleanup of outdated data (> windowMs + 10 seconds)
  • Logging of heavy requests when exceeding heavyPercent

3. AdaptiveDelayer (Adaptive Delays)

Purpose: Dynamically calculates delays based on previous request execution experience.

AdaptiveDelayer — Configuration

interface AdaptiveConfig {
  enabled: boolean        // Enable adaptive delays
  thresholdPercent: number // Activation threshold (% of operating limit)
  coefficient: number     // Delay multiplier (0.01 = 1% of remaining blocking time)
  maxDelay: number        // Maximum delay in milliseconds
}

AdaptiveDelayer — Delay Calculation Algorithm

If operating of current method > (limitMs × thresholdPercent / 100):
  If operating_reset_at > current time:
    Delay = (operating_reset_at - current time) × coefficient
  Otherwise:
    Delay = 7000 ms (default value)
  
  Final delay = min(calculated, maxDelay)

AdaptiveDelayer — Features

  • For batch requests, selects the maximum delay among all methods
  • Does not block execution, only adds delay
  • Uses statistics from OperatingLimiter

4. RestrictionManager (Main Coordinator)

Purpose: Manages all types of restrictions and error handling.

Order of Applying Restrictions

  1. Operating Limit Check: Checks operation time limit
  2. Adaptive Delay: Applies adaptive delay if necessary
  3. Rate Limit: Checks and applies rate limiting (loop for parallel requests)

Error Handling

// Determining error type
#isRateLimitError(error): boolean          // 503 or QUERY_LIMIT_EXCEEDED
#isOperatingLimitError(error): boolean     // 429 or OPERATION_TIME_LIMIT
#isNonRetryableClientError(error): boolean // HTTP 4xx (except 429 / 408)
#isNeedThrowError(error): boolean          // Critical errors (no point in retrying)

Retry Strategy

  1. For limit errors: Exponential delay considering the attempt number
  2. For client errors (HTTP 4xx, except 429 / 408): No retries — a 4xx response is deterministic, so retrying cannot change the outcome. The error exits the retry loop on the first attempt; the usual hard/soft classification then applies (thrown as AjaxError, or returned inside AjaxResult for soft codes). 429 is retried as a rate/operating limit; 408 (request timeout) stays transient.
  3. For other errors: Basic backoff with jitter (±10%)
  4. Critical errors: No retries

Default Configurations

Default parameters for regular tariffs (standard)

{
  rateLimit: {
    burstLimit: 50,      // 50 simultaneous requests
    drainRate: 2,        // 2 requests per second
    adaptiveEnabled: true
  },
  operatingLimit: {
    windowMs: 600000,    // 10 minutes
    limitMs: 480000,     // 480 seconds (8 minutes)
    heavyPercent: 80     // Notification at 80% usage
  },
  adaptiveConfig: {
    enabled: true,
    thresholdPercent: 80, // Activation at 80% of limit
    coefficient: 0.01,    // 1% of remaining blocking time
    maxDelay: 7000        // Maximum 7 seconds delay
  },
  maxRetries: 3,
  retryDelay: 1000,
  retryOnNetworkError: true
}

Parameters for the Enterprise plan

{
  ...standard,
  rateLimit: {
    burstLimit: 250,     // 250 simultaneous requests
    drainRate: 5,        // 5 requests per second
    adaptiveEnabled: true
  }
}

Parameters for bulk data processing

{
  ...standard,
  rateLimit: {
    burstLimit: 30,
    drainRate: 1,
    adaptiveEnabled: true
  },
  operatingLimit: {
    windowMs: 600_000,
    limitMs: 480_000,
    heavyPercent: 50 // Higher threshold for notifications
  },
  adaptiveConfig: {
    enabled: true,
    thresholdPercent: 50, // More threshold
    coefficient: 0.015, // More pause
    maxDelay: 10_000 // Max 10 seconds
  },
  maxRetries: 5 // More attempts
}

Real-time parameters

{
  ...standard,
  adaptiveConfig: {
    enabled: false, // Off
    thresholdPercent: 100,
    coefficient: 0.001,
    maxDelay: 480_000
  },
  maxRetries: 1
}

Monitoring and Statistics

Getting Statistics

import { ApiVersion } from '@bitrix24/b24jssdk'

// const $b24 = ...

const statsV2 = $b24.getHttpClient(ApiVersion.v2).getStats()
const statsV3 = $b24.getHttpClient(ApiVersion.v3).getStats()

Statistics structure:

{
  // General statistics
  retries: number,                 // Number of retry attempts
  consecutiveErrors: number,       // Consecutive errors
  limitHits: number,               // Limit hits
  
  // Rate Limiter
  tokens: number,                  // Current number of tokens
  burstLimit: number,              // Current burst limit
  drainRate: number,               // Current drain rate
  
  // Adaptive Delayer
  adaptiveDelays: number,          // Number of applied delays
  totalAdaptiveDelay: number,      // Total delay time
  adaptiveDelayAvg: number,        // Average delay
  
  // Operating Limiter
  heavyRequestCount: number,       // Number of heavy requests
  operatingStats: {                // Method statistics (in seconds)
    [method: string]: number
  },
  
  // Errors by method
  errorCounts: {
    [method: string]: number
  }
}

Resetting Statistics

// Complete reset of all limiter statistics
import { ApiVersion } from '@bitrix24/b24jssdk'

// const $b24 = ...

await $b24.getHttpClient(ApiVersion.v2).reset()
await $b24.getHttpClient(ApiVersion.v3).reset()

Long-Running Requests & Non-idempotent Calls

The SDK ships with a 30-second axios timeout and retries failed requests up to maxRetries times. Both defaults are wrong for long-running, non-idempotent REST methods — the canonical example is crm.documentgenerator.document.add, which can take 10‒60 seconds to render a template and is the original report behind issue #24.

Why it goes wrong by default

When the server takes longer than the axios timeout, this sequence plays out:

  1. Client opens the request, the server begins processing.
  2. Axios fires its timeout, the SDK sees REQUEST_TIMEOUT (code: ECONNABORTED).
  3. REQUEST_TIMEOUT is treated as a transient error → SDK retries.
  4. The server, unaware that the client gave up, finishes the first request and persists a document. Then it accepts the retry and persists another.
  5. After maxRetries attempts the SDK throws JSSDK_CALL_ALL_ATTEMPTS_EXHAUSTED and the caller is left with 2-3 duplicates in CRM.

The same risk applies to every non-idempotent method: crm.deal.add, crm.contact.add, disk.folder.uploadfile, tasks.task.add, any custom REST endpoint that creates state.

Use this pattern for any call that creates an entity, regardless of expected duration:

import { ApiVersion, ParamsFactory } from '@bitrix24/b24jssdk'

// const $b24 = ...

// 1. Raise the timeout so the client actually waits for a slow operation.
const clientAxios = $b24.getHttpClient(ApiVersion.v2).ajaxClient
clientAxios.defaults.timeout = 120_000 // default is 30_000

// 2. Disable retries on transport errors so a client-side timeout never
//    creates duplicate entities on the server.
await $b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),
  retryOnNetworkError: false
})

// Now safe for non-idempotent calls.
const result = await $b24.actions.v2.call.make({
  method: 'crm.documentgenerator.document.add',
  params: { templateId: 42, entityTypeId: 2, entityId: 6014, values: {} }
})
Either step alone is not enough. A long timeout without retryOnNetworkError: false still produces duplicates on a flaky network (the server replied, but the response never arrived). The flag without the timeout fails too eagerly on requests that would have completed in 35-40 seconds.

Targeted use — per-call override

If you only need the strict behaviour for a specific code path, build a fresh B24Hook for that call instead of mutating the global one:

import { B24Hook, ParamsFactory } from '@bitrix24/b24jssdk'
import { ApiVersion } from '@bitrix24/b24jssdk'

const $b24Strict = B24Hook.fromWebhookUrl(hookUrl, {
  restrictionParams: {
    ...ParamsFactory.getDefault(),
    retryOnNetworkError: false
  }
})
$b24Strict.getHttpClient(ApiVersion.v2).ajaxClient.defaults.timeout = 120_000

await $b24Strict.actions.v2.call.make({ method: 'crm.deal.add', params: {/*…*/} })

Last resort — disable all retries

If you cannot afford any retry under any circumstances (e.g. a billing operation), set maxRetries: 1:

import { ParamsFactory } from '@bitrix24/b24jssdk'

await $b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),
  maxRetries: 1
})

This affects every error class, not just transport errors, so use sparingly.

Customizing Error Classification

The SDK groups REST error codes into three categories:

  1. Hard errors — thrown immediately as AjaxError. No retry. Used for fatal conditions (authorization failures, invalid arguments, deleted portals).
  2. Soft errors — returned inside AjaxResult as an error payload, not thrown. Used for codes that callers typically inspect as part of normal control flow (e.g. ENTITY_NOT_FOUND, v3 validation errors).
  3. Everything else — retryability is decided by HTTP status. Client errors (HTTP 4xx, except 429 and 408) are never retried — they are deterministic, so the SDK fails fast on the first attempt regardless of whether the error code is enumerated. Transient conditions (5xx, 429, 408, network errors) are retried up to maxRetries times with backoff and jitter.

The built-in lists cover Bitrix24's standard REST surface. If your application uses custom REST endpoints (e.g. from a local app or a placement handler) that return their own error codes with a non-4xx status, those codes will be retried by default — which is wrong for non-idempotent business errors.

Extend the classification via hardErrorCodes and softErrorCodes:

import { ParamsFactory } from '@bitrix24/b24jssdk'

// const $b24 = ...

await $b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),

  // These will throw immediately instead of being retried:
  hardErrorCodes: [
    'DOCUMENT_GENERATOR_ALREADY_IN_QUEUE', // business code: don't retry
    'MY_APP_INVALID_PAYLOAD'               // custom REST: caller fix needed
  ],

  // These will be returned in AjaxResult instead of thrown:
  softErrorCodes: [
    'MY_APP_VALIDATION_FAILED'             // expected via normal flow
  ]
})
Merge, not replace. User-provided codes are appended to the built-in lists. You can only add codes — the built-ins (authorization codes, INTERNAL_SERVER_ERROR, v3 validation codes, etc.) stay in place. This prevents accidentally disabling critical safeguards like expired_token detection.

The built-in lists are exposed as static fields for reference:

import { RestrictionManager } from '@bitrix24/b24jssdk'

console.log(RestrictionManager.BUILT_IN_HARD_ERROR_CODES)
console.log(RestrictionManager.BUILT_IN_SOFT_ERROR_CODES)

Usage Recommendations

Configuration for different scenarios:

import { ParamsFactory } from '@bitrix24/b24jssdk'

// const $b24 = ...

// Default parameters
$b24.setRestrictionManagerParams( ParamsFactory.getDefault() )

// Batch processing
$b24.setRestrictionManagerParams( ParamsFactory.getBatchProcessing() )

// Real-time
$b24.setRestrictionManagerParams( ParamsFactory.getRealtime() )

// By tariff plan
$b24.setRestrictionManagerParams( ParamsFactory.fromTariffPlan('enterprise') )

// Dynamic configuration change
$b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),
  rateLimit: {
    burstLimit: 30,  // Temporary reduction in case of problems
    drainRate: 1,
    adaptiveEnabled: true
  }
})