<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[WebSocket в Node.js жрёт память: утечки EventEmitter]]></title><description><![CDATA[<p dir="auto">WebSocket-соединения в Node.js — это мина под performance вашего сервера. Казалось бы, простая подписка на события, переподключение при падении соединения, обработка данных. Но где-то между <code>client.on('error')</code> и рекурсивным вызовом функции подключения память начинает расти как на дрожжах. За пару часов работы 90% оперативки занято, приложение тупит, а в логах весело мигает предупреждение про EventEmitter.</p>
<p dir="auto">Проблема не в самом Node.js и даже не в WebSocket. Проблема в том, как мы обрабатываем события. EventEmitter держит все подписчики в памяти, пока они не удалены явно. И если вы забыли про <code>removeAllListeners()</code> в нужном месте — поздравляем, у вас классическая утечка памяти. Разберёмся, почему это происходит и как это вообще предотвратить.</p>
<h2>Как EventEmitter тихо душит вашу память</h2>
<p dir="auto">На первый взгляд код выглядит безобидно. Есть функция переподключения, которая срабатывает каждую секунду, если соединение упало. Тайм-аут, снова попытка, и так по кругу. Но вот что происходит под капотом: каждый раз, когда вы вызываете <code>client.on('error', callback)</code>, вы добавляете новый обработчик в <strong>внутренний массив слушателей</strong> объекта client.</p>
<p dir="auto">Этот массив живёт столько же, сколько живёт сам объект. И если объект долго живущий (например, socket, который переиспользуется для переподключений), то все эти обработчики накапливаются в памяти как снежный ком. Первая итерация — один обработчик, вторая — два, третья — три. Через час таких переподключений у вас уже тысячи мёртвых обработчиков.</p>
<p dir="auto">Вот типичный сценарий:</p>
<pre><code class="language-javascript">var net = require('net');
const client = new net.Socket();

let _connect_ = function () {
  client.connect(port, host, function (err) {
    if (err) {
      console.log("error");
      console.log(err);
    } else {
      console.log("connected");
    }
  });
  
  client.on('error', function () {
    console.log("connection error");
    setTimeout(function(){_connect_();}, 1000);
  });
};

_connect_();
</code></pre>
<p dir="auto">Выглядит нормально, не так ли? Но нет. Каждый вызов <code>_connect_()</code> добавляет новый обработчик <code>'error'</code>. Если сервер падает часто, обработчиков станет столько же, сколько попыток переподключения. И память будет расти линейно, пока не станет критичной.</p>
<p dir="auto"><strong>Проблема здесь двойная:</strong> во-первых, обработчики накапливаются. Во-вторых, если вы добавили ещё и обработчики на <code>'data'</code>, <code>'close'</code>, <code>'timeout'</code> — их количество растёт кратно. Пять событий по тысяче обработчиков каждое — это уже 5000 функций в памяти, которые никогда не будут вызваны.</p>
<h2>Когда Node.js начинает кричать о проблеме</h2>
<p dir="auto">Node.js имеет встроенный детектор утечек памяти через EventEmitter. По умолчанию максимальное количество обработчиков на один объект — <strong>10</strong>. Как только вы превышаете этот лимит, система выплёвывает вот такое предупреждение:</p>
<pre><code>(node) warning: possible EventEmitter memory leak detected. 11 listeners added.
Use emitter.setMaxListeners() to increase limit.
</code></pre>
<p dir="auto">Звучит как помощь, но это ловушка. Новички видят это предупреждение, гуглят и находят совет: просто вызови <code>setMaxListeners()</code> и забей. Вот так:</p>
<pre><code class="language-javascript">client.setMaxListeners(100);
</code></pre>
<p dir="auto">Ой, проблема решена! Предупреждение исчезло. Но утечка-то остаётся. Вы просто заткнули рот детектору, но инструмент продолжает жрать память так же интенсивно. Это как отключить датчик масла в двигателе машины — проблема не исчезла, просто вы её не видите.</p>
<p dir="auto">Предупреждение — это <strong>сигнал</strong>, что что-то не так с вашей архитектурой. А не сигнал к тому, чтобы увеличить лимит:</p>
<ul>
<li>Предупреждение означает, что вы привязали коротко живущие объекты к долго живущим.</li>
<li>Это признак того, что обработчики где-то не удаляются после использования.</li>
<li>Это подсказка, что нужно переписать логику переподключения или управления lifecycle’ом.</li>
</ul>
<h2>Как правильно управлять переподключениями</h2>
<p dir="auto">Решение простое — <strong>очищайте слушателей перед добавлением новых</strong>. Используйте <code>removeAllListeners()</code> перед тем, как снова вызывать <code>on()</code>:</p>
<pre><code class="language-javascript">var net = require('net');
const client = new net.Socket();

let _connect_ = function () {
  client.connect(port, host, function (err) {
    if (err) {
      console.log("error");
      console.log(err);
    } else {
      console.log("connected");
    }
  });
  
  client.on('error', function () {
    console.log("connection error");
    reconnect();
  });
};

function reconnect() {
  client.removeAllListeners();
  client.on('error', function () {
    console.log("connection error");
  });
  setTimeout(function(){_connect_();}, 1000);
}

_connect_();
</code></pre>
<p dir="auto">Теперь каждый раз, когда вы переподключаетесь, все старые обработчики удаляются. Масив слушателей очищается, и память не растёт бесконечно.</p>
<p dir="auto">Лучший подход — <strong>минимизировать переиспользование объектов</strong>. Вот альтернатива:</p>
<ul>
<li>Создавайте новый socket для каждого переподключения вместо переиспользования старого.</li>
<li>Явно закрывайте соединение перед новой попыткой: <code>client.destroy()</code>.</li>
<li>Используйте паттерн с Promise или async/await, где lifetime обработчиков ограничен одной операцией.</li>
</ul>
<pre><code class="language-javascript">async function connectWithRetry(host, port, maxRetries = 5) {
  for (let i = 0; i &lt; maxRetries; i++) {
    try {
      return await new Promise((resolve, reject) =&gt; {
        const socket = new net.Socket();
        
        const timeout = setTimeout(() =&gt; {
          socket.destroy();
          reject(new Error('Connection timeout'));
        }, 5000);
        
        socket.once('connect', () =&gt; {
          clearTimeout(timeout);
          resolve(socket);
        });
        
        socket.once('error', reject);
        socket.connect(port, host);
      });
    } catch (err) {
      if (i === maxRetries - 1) throw err;
      await new Promise(r =&gt; setTimeout(r, 1000));
    }
  }
}
</code></pre>
<p dir="auto">Здесь каждая попытка создаёт <strong>новый socket</strong> и использует <code>once()</code> вместо <code>on()</code>. Это автоматически удаляет обработчик после первого срабатывания. Нет накопления, нет утечек.</p>
<h2>Что ещё создаёт скрытые утечки</h2>
<p dir="auto">EventEmitter-утечки — это не единственная проблема. Есть и другие подводные камни:</p>
<p dir="auto"><strong>Забытые таймауты и интервалы.</strong> Если вы создаёте <code>setInterval()</code> или <code>setTimeout()</code> в обработчике события, но забываете их очищать, они тоже накапливаются в памяти. Каждый таймер — это ссылка на функцию, которая удерживает scope в памяти.</p>
<p dir="auto"><strong>Замыкания с большими объектами.</strong> Если ваш обработчик события сохраняет в замыкании большой объект (например, весь запрос или весь буфер данных), этот объект не будет освобождён, пока жив обработчик.</p>
<p dir="auto"><strong>Неудалённые слушатели в цепи.</strong> Если вы привязываете слушателей к слушателям (слушатель добавляет нового слушателя), и это происходит во вложенном коде, получится пирамида утечек.</p>
<p dir="auto"><strong>Socket-ы без явного закрытия.</strong> Если не вызвать <code>socket.destroy()</code> или <code>socket.end()</code> при обработке <code>timeout</code>-события, сокет останется в памяти в полу-мёртвом состоянии.</p>
<p dir="auto">Как отследить эти утечки? Есть несколько способов:</p>
<ul>
<li><strong>Heapdump.</strong> Модуль, который делает снимок памяти Node.js и позволяет анализировать его в Chrome DevTools. Можно сравнить два снимка и увидеть, какие объекты растут.</li>
<li><strong>Node.js --inspect.</strong> Встроенный инструмент для профилирования. Запустите приложение с флагом <code>--inspect</code> и подключитесь к inspector’у.</li>
<li><strong>Простой мониторинг.</strong> Выводите <code>process.memoryUsage()</code> каждые 5 секунд. Если числа растут монотонно и никогда не падают — утечка.</li>
</ul>
<h2>За пределами EventEmitter</h2>
<p dir="auto">Меморная оптимизация WebSocket-соединений в Node.js — это целая область. Вы можете глубже упасть в кроличью нору и наткнуться на проблемы типа утечек в буферах данных, неправильного управления потоками (streams), или неудачной компиляции V8. Но если вы начали с проблемы расхода памяти — 90% вероятности, что виноват именно EventEmitter и забытые обработчики.</p>
<p dir="auto">Суть простая: <strong>каждый <code>on()</code> должен иметь парный <code>removeListener()</code> или <code>removeAllListeners()</code></strong>, или вы должны использовать <code>once()</code> вместо <code>on()</code>. Это базовый принцип работы с событиями в Node.js, и его неуважение ведёт к той самой ситуации, когда сервер жрёт 90% памяти и падает через час работы.</p>
]]></description><link>https://forum.exlends.ru/topic/1984/websocket-v-node.js-zhryot-pamyat-utechki-eventemitter</link><generator>RSS for Node</generator><lastBuildDate>Wed, 20 May 2026 06:38:16 GMT</lastBuildDate><atom:link href="https://forum.exlends.ru/topic/1984.rss" rel="self" type="application/rss+xml"/><pubDate>Sat, 28 Mar 2026 10:24:19 GMT</pubDate><ttl>60</ttl></channel></rss>