FastifySeniorExperience

Какие production-риски есть у Fastify: blocking code, connection pooling, config, auth, observability, deploy или graceful shutdown?

Production-риски Fastify: синхронный блокирующий код в async handler, отсутствие connection pool настройки в плагинах БД, небезопасные defaults в JWT, пропущенные lifecycle hooks для observability и некорректный graceful shutdown без дренажа соединений.

Обзор production-рисков Fastify

Fastify — неблокирующий фреймворк, но он не защищает от ошибок, которые разработчик вносит сам. Рассмотрим каждую категорию риска с конкретными примерами и решениями.

1. Blocking code

Синхронные операции в async handler блокируют event loop Node.js целиком:

// ПЛОХО
app.get('/compute', async (req, reply) => {
  const result = heavyCryptoSync(req.body.data); // блокирует ~200ms
  return result;
});

// ХОРОШО — offload в worker thread
import { Worker } from 'worker_threads';
app.get('/compute', async (req, reply) => {
  const result = await runInWorker('./heavy.js', req.body.data);
  return result;
});

Инструмент диагностики: --prof флаг Node.js + node --prof-process, либо clinic.js doctor.

2. Connection pooling

Плагин @fastify/postgres использует pg pool. Дефолтный размер — 10 соединений, что мало для нагрузки:

await app.register(fastifyPostgres, {
  connectionString: process.env.DATABASE_URL,
  pool: {
    max: 20,          // под нагрузку
    min: 2,
    idleTimeoutMillis: 30_000,
    connectionTimeoutMillis: 5_000,
  },
});

Аналогично для Redis через @fastify/redis — настраивайте lazyConnect: true и обрабатывайте error события.

3. Config и секреты

Используйте @fastify/env с JSON Schema для валидации переменных окружения при старте:

await app.register(fastifyEnv, {
  schema: Type.Object({
    PORT: Type.Integer({ default: 3000 }),
    JWT_SECRET: Type.String({ minLength: 32 }),
    DATABASE_URL: Type.String({ format: 'uri' }),
  }),
  dotenv: true,
});

Если секрет не задан, приложение падает сразу — это лучше, чем runtime undefined.

4. Auth

@fastify/jwt по умолчанию не проверяет exp, если не передать { complete: true }. Используйте preHandler:

app.addHook('preHandler', async (req, reply) => {
  try {
    await req.jwtVerify(); // бросает если exp истёк
  } catch (err) {
    reply.code(401).send({ error: 'Unauthorized' });
  }
});

Применяйте только к защищённым маршрутам через onRoute hook или отдельный plugin с prefix.

5. Observability

Fastify логирует через Pino. Добавьте reqId в каждый лог и интегрируйте с OpenTelemetry:

import { trace } from '@opentelemetry/api';

app.addHook('onRequest', async (req) => {
  const span = trace.getActiveSpan();
  span?.setAttribute('http.route', req.routerPath);
});

app.addHook('onResponse', async (req, reply) => {
  req.log.info({
    statusCode: reply.statusCode,
    responseTime: reply.elapsedTime,
  }, 'request completed');
});

6. Graceful Shutdown

const signals = ['SIGTERM', 'SIGINT'];
for (const signal of signals) {
  process.on(signal, async () => {
    app.log.info('Shutting down...');
    await app.close(); // дренирует keep-alive соединения
    process.exit(0);
  });
}

app.close() вызывает хуки onClose плагинов — пул БД закрывается корректно.

Подводные камни

  • Unhandled promise rejection в Fastify 5 завершает процесс — оберните все async handler в try/catch или используйте глобальный setErrorHandler.
  • reply.send() после reply.hijack() вызывает исключение — частая ошибка при работе с WebSocket.
  • Pino async transport (worker thread) при резком завершении процесса теряет последние строки лога — используйте pino.final в обработчике сигналов.
  • JWT refresh token rotation без Redis blacklist приводит к невозможности инвалидировать токены при компрометации.
  • Метрики Prometheus через fastify-metrics по умолчанию открыты на том же порту — нужен отдельный internal server или IP-whitelist.
  • При горизонтальном масштабировании rate-limit плагина (@fastify/rate-limit) без Redis-стора лимиты считаются на каждый pod отдельно.
  • Database connection pool size × число pod не должно превышать max_connections PostgreSQL — иначе новые соединения получат ошибку.
  • Keep-alive timeout Fastify (72s по умолчанию) должен быть меньше таймаута upstream load balancer — иначе 502 при idle соединениях.

What hurts your answer

  • Говорить только о запуске Fastify, но не об эксплуатации
  • Не упоминать observability, обновления, безопасность и rollback
  • Описывать риски абстрактно, без способов их снижать

What they're listening for

  • Видит production-риски Fastify
  • Говорит про monitoring, rollout, rollback и безопасность
  • Умеет ранжировать риски по вероятности и влиянию

Related topics