Ограничение частоты запросов

Ограничение частоты запросов (Rate Limiting) — механизм контроля количества сообщений, отправляемых клиентом через STOMP-соединение за определённый промежуток времени. В контексте STOMP.js этот механизм особенно важен при работе с:

  • чатами;
  • системами уведомлений;
  • потоковой телеметрией;
  • игровыми серверами;
  • финансовыми системами;
  • системами реального времени.

Без ограничения частоты клиент способен:

  • перегрузить брокер сообщений;
  • создать сетевой шторм;
  • вызвать лавинообразный рост очередей;
  • спровоцировать блокировки на сервере;
  • создать DoS-подобную нагрузку;
  • нарушить лимиты API.

Даже один ошибочный цикл setInterval() способен генерировать тысячи сообщений в секунду.


Проблемы при отсутствии Rate Limiting

Переполнение очередей

Если сообщения отправляются быстрее, чем брокер успевает их обрабатывать, очередь начинает стремительно расти.

Пример проблемы:

setInterval(() => {
    client.publish({
        destination: '/topic/data',
        body: JSON.stringify(generateData())
    });
}, 1);

Такой код создаёт до 1000 сообщений в секунду.


Перегрузка WebSocket

STOMP.js работает поверх WebSocket. При слишком высокой скорости передачи возникают:

  • рост задержек;
  • потеря пакетов;
  • фрагментация кадров;
  • увеличение памяти браузера;
  • блокировка event loop.

DDoS-подобное поведение

Ошибки фронтенда могут выглядеть как атака:

while (true) {
    client.publish({
        destination: '/topic/logs',
        body: 'spam'
    });
}

Некоторые брокеры автоматически разрывают соединение.


Рост потребления памяти

STOMP.js временно хранит данные в памяти JavaScript-движка. Если отправка быстрее сети, формируется внутренний буфер.

Последствия:

  • рост RAM;
  • зависание вкладки;
  • GC-паузы;
  • падение браузера.

Основные стратегии ограничения

Fixed Window

Фиксированное окно времени.

Например:

  • не более 100 сообщений в секунду;
  • счётчик обнуляется каждую секунду.

Схема:

0s ------- 1s ------- 2s
| 100 req | 100 req |

Недостаток — всплески на границе окна.


Sliding Window

Скользящее окно анализирует последние N миллисекунд.

Пример:

Последние 1000 ms → максимум 100 сообщений

Механизм более точный и плавный.


Token Bucket

Одна из лучших стратегий для STOMP.

Принцип:

  • клиент получает токены;
  • каждое сообщение тратит токен;
  • токены восстанавливаются постепенно;
  • при отсутствии токенов отправка блокируется.

Leaky Bucket

Имитирует постоянную скорость утечки.

Даже если клиент создаёт всплеск, сообщения выходят равномерно.

Подходит для:

  • телеметрии;
  • логирования;
  • метрик;
  • IoT.

Простое ограничение через timestamp

Базовая реализация

import { Client } fr om '@stomp/stompjs';

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws'
});

let lastSend = 0;
const LIMIT_MS = 1000;

function safePublish(message) {
    const now = Date.now();

    if (now - lastSend < LIMIT_MS) {
        console.warn('Лимит превышен');
        return;
    }

    lastSend = now;

    client.publish({
        destination: '/topic/messages',
        body: JSON.stringify(message)
    });
}

Недостатки подхода

Подобный вариант:

  • разрешает только одно сообщение за интервал;
  • не поддерживает burst-режим;
  • не масштабируется;
  • неудобен для нескольких каналов.

Ограничение через счётчик запросов

Лимит сообщений в секунду

let counter = 0;

setInterval(() => {
    counter = 0;
}, 1000);

function publishLimited(body) {

    if (counter >= 10) {
        console.warn('Слишком много сообщений');
        return;
    }

    counter++;

    client.publish({
        destination: '/topic/chat',
        body
    });
}

Особенности метода

Преимущества:

  • простота;
  • минимальная нагрузка;
  • высокая скорость.

Недостатки:

  • резкие всплески;
  • нет плавности;
  • нет приоритетов.

Реализация Token Bucket

Основная идея

Пусть:

  • максимум 20 токенов;
  • каждую секунду восстанавливается 5 токенов;
  • одно сообщение потребляет 1 токен.

Реализация

class TokenBucket {

    constructor(maxTokens, refillRate) {
        this.maxTokens = maxTokens;
        this.tokens = maxTokens;
        this.refillRate = refillRate;

        setInterval(() => {
            this.tokens = Math.min(
                this.maxTokens,
                this.tokens + this.refillRate
            );
        }, 1000);
    }

    consume(count = 1) {

        if (this.tokens < count) {
            return false;
        }

        this.tokens -= count;

        return true;
    }
}

Использование со STOMP.js

const bucket = new TokenBucket(20, 5);

function publish(body) {

    if (!bucket.consume()) {
        console.warn('Rate lim it exceeded');
        return;
    }

    client.publish({
        destination: '/topic/events',
        body
    });
}

Поведение системы

Если клиент простаивал:

  • токены накапливаются;
  • затем разрешается burst-отправка.

Это удобно для:

  • периодических обновлений;
  • пакетной отправки;
  • буферизированных событий.

Очередь сообщений с ограничением скорости

Проблема мгновенного отклонения

Иногда нельзя терять сообщения.

Вместо:

return;

лучше помещать сообщение в очередь.


Реализация очереди

class PublishQueue {

    constructor(ratePerSecond) {
        this.queue = [];
        this.interval = 1000 / ratePerSecond;

        this.start();
    }

    enqueue(message) {
        this.queue.push(message);
    }

    start() {

        setInterval(() => {

            if (this.queue.length === 0) {
                return;
            }

            const message = this.queue.shift();

            client.publish({
                destination: message.destination,
                body: message.body
            });

        }, this.interval);
    }
}

Использование

const queue = new PublishQueue(5);

queue.enqueue({
    destination: '/topic/chat',
    body: 'hello'
});

Преимущества очереди

Очередь обеспечивает:

  • стабильную нагрузку;
  • предсказуемую скорость;
  • отсутствие burst-всплесков;
  • сглаживание нагрузки.

Debounce и Throttle

Debounce

Debounce откладывает выполнение до завершения серии вызовов.

Пример:

function debounce(fn, delay) {

    let timeout;

    return (...args) => {

        clearTimeout(timeout);

        timeout = setTimeout(() => {
            fn(...args);
        }, delay);
    };
}

Использование для STOMP.js

const sendTyping = debounce(() => {

    client.publish({
        destination: '/topic/typing',
        body: 'typing'
    });

}, 500);

Где применяется debounce

Подходит для:

  • индикатора печати;
  • поиска;
  • UI-событий;
  • изменения размеров окна.

Throttle

Throttle ограничивает частоту выполнения.

function throttle(fn, limit) {

    let waiting = false;

    return (...args) => {

        if (waiting) {
            return;
        }

        fn(...args);

        waiting = true;

        setTimeout(() => {
            waiting = false;
        }, limit);
    };
}

Использование throttle

const sendMouse = throttle((position) => {

    client.publish({
        destination: '/topic/mouse',
        body: JSON.stringify(position)
    });

}, 100);

Где полезен throttle

Особенно эффективен для:

  • координат мыши;
  • drag-and-drop;
  • сенсоров;
  • потоковых данных.

Ограничение по размеру полезной нагрузки

Rate limiting касается не только количества сообщений.

Важно ограничивать:

  • размер payload;
  • общий объём трафика;
  • скорость передачи байтов.

Проверка размера сообщения

function publishSafe(body) {

    const bytes = new Blob([body]).size;

    if (bytes > 1024 * 10) {
        throw new Error('Payload too large');
    }

    client.publish({
        destination: '/topic/data',
        body
    });
}

Комбинированные ограничения

На практике используется несколько уровней защиты одновременно.

Пример:

  • не более 20 сообщений/сек;
  • не более 100 KB/сек;
  • не более 10 MB в минуту;
  • не более 1 reconnect в 5 секунд.

Ограничение reconnect-попыток

Проблема reconnect storm

При падении брокера тысячи клиентов начинают переподключение одновременно.

Это создаёт:

  • лавинообразную нагрузку;
  • новые сбои;
  • циклические отключения.

Неправильный подход

client.reconnectDelay = 100;

Слишком агрессивное переподключение.


Exponential Backoff

Правильнее увеличивать задержку постепенно.

let reconnectDelay = 1000;

client.onWebSocketCl ose = () => {

    setTimeout(() => {

        client.activate();

        reconnectDelay *= 2;

        reconnectDelay = Math.min(reconnectDelay, 30000);

    }, reconnectDelay);
};

Ограничение подписок

Некоторые приложения динамически создают подписки.

Ошибка:

setInterval(() => {

    client.subscribe('/topic/random', () => {});

}, 10);

Это вызывает:

  • утечки памяти;
  • рост числа consumer;
  • перегрузку брокера.

Контроль количества подписок

const subscriptions = new Map();
const MAX_SUBSCRIPTIONS = 50;

function safeSubscribe(destination, callback) {

    if (subscriptions.size >= MAX_SUBSCRIPTIONS) {
        throw new Error('Subscription limit exceeded');
    }

    const sub = client.subscribe(destination, callback);

    subscriptions.set(destination, sub);

    return sub;
}

Серверные ограничения

Ограничения только на клиенте недостаточны

Клиентский код можно:

  • изменить;
  • отключить;
  • подделать.

Настоящий контроль выполняется на сервере.


RabbitMQ

В RabbitMQ используются:

  • prefetch;
  • policy;
  • channel limits;
  • connection limits.

ActiveMQ

ActiveMQ поддерживает:

  • producer flow control;
  • memory limits;
  • pending message limits.

Spring WebSocket

Spring позволяет:

  • ограничивать inbound rate;
  • блокировать flood;
  • ограничивать размер STOMP frame;
  • отключать клиентов.

Обнаружение flood-атак

Аномальное поведение клиента

Подозрительные признаки:

  • одинаковые сообщения;
  • высокая скорость;
  • burst-активность;
  • постоянные reconnect;
  • тысячи подписок.

Простейший анализ

const history = [];

function trackMessage() {

    const now = Date.now();

    history.push(now);

    while (history[0] < now - 1000) {
        history.shift();
    }

    if (history.length > 100) {
        console.warn('Flood detected');
    }
}

Rate Limiting через RxJS

RxJS хорошо подходит для потокового ограничения.


ThrottleTime

import { Subject } from 'rxjs';
import { throttleTime } from 'rxjs/operators';

const stream = new Subject();

stream
    .pipe(throttleTime(100))
    .subscribe(message => {

        client.publish({
            destination: '/topic/data',
            body: message
        });

    });

BufferTime

import { bufferTime } from 'rxjs/operators';

stream
    .pipe(bufferTime(1000))
    .subscribe(messages => {

        client.publish({
            destination: '/topic/batch',
            body: JSON.stringify(messages)
        });

    });

Производительность и Rate Limiting

Грамотно настроенное ограничение:

  • уменьшает latency;
  • стабилизирует нагрузку;
  • снижает потребление памяти;
  • уменьшает сетевой шум;
  • предотвращает перегрузку брокера;
  • повышает стабильность WebSocket.

Архитектура промышленного ограничения

Крупные системы обычно используют:

  1. Локальный limiter в браузере.
  2. Ограничение на API Gateway.
  3. Ограничение в брокере.
  4. Контроль consumer rate.
  5. Мониторинг аномалий.
  6. Автоматический ban flood-клиентов.
  7. Адаптивные лимиты.
  8. Приоритетные очереди.

Практические рекомендации

Для чатов

Подходящие лимиты:

  • 3–5 сообщений в секунду;
  • debounce для typing events;
  • ограничение размера сообщений.

Для телеметрии

Рекомендуется:

  • batching;
  • throttle;
  • compression;
  • queue buffering.

Для real-time UI

Подходят:

  • throttle 16–100 ms;
  • coalescing событий;
  • dropping устаревших данных.

Для финансовых систем

Важно:

  • гарантировать доставку;
  • использовать очередь;
  • не терять сообщения;
  • ограничивать burst-активность;
  • логировать превышения лимитов.