From 817f416b79d6da9e1b9763f7e5c1a1e4c0b81191 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 8 Jun 2026 00:10:37 -0400 Subject: [PATCH] Harden API process under production load --- .env.example | 11 ++++++ docker-compose.external-db.yml | 11 ++++++ docker-compose.yml | 11 ++++++ ecosystem.config.js | 15 ++++++++ full-setup/docker-compose-mainnet.yml | 13 +++++++ src/main.ts | 50 +++++++++++++++++++++++---- 6 files changed, 105 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 756a6d8..4937c1b 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,17 @@ API_PORT=3334 # Keep API_BIND_HOST as 127.0.0.1 when a reverse proxy terminates public traffic. API_BIND_HOST=127.0.0.1 API_PUBLIC_PORT=3334 +API_MAX_CONNECTIONS=512 +API_REQUEST_TIMEOUT_MS=15000 +API_HEADERS_TIMEOUT_MS=10000 +API_KEEP_ALIVE_TIMEOUT_MS=5000 +API_SOCKET_TIMEOUT_MS=15000 +API_TLS_HANDSHAKE_TIMEOUT_MS=3000 +API_LISTEN_BACKLOG=1024 +API_NODE_ARGS=--max-old-space-size=512 +API_MAX_MEMORY_RESTART=768M +API_KILL_TIMEOUT_MS=5000 +API_RESTART_DELAY_MS=2000 # Plain TCP Stratum ports accept both SV1 JSON-RPC and SV2 Noise/binary traffic. STRATUM_PORTS=3333,3332,3331,3330 diff --git a/docker-compose.external-db.yml b/docker-compose.external-db.yml index 7a4830d..4a842fc 100644 --- a/docker-compose.external-db.yml +++ b/docker-compose.external-db.yml @@ -50,6 +50,17 @@ services: REDIS_URL: ${REDIS_URL:-redis://redis:6379} API_PORT: ${API_PORT:-3334} API_SECURE: ${API_SECURE:-false} + API_MAX_CONNECTIONS: ${API_MAX_CONNECTIONS:-512} + API_REQUEST_TIMEOUT_MS: ${API_REQUEST_TIMEOUT_MS:-15000} + API_HEADERS_TIMEOUT_MS: ${API_HEADERS_TIMEOUT_MS:-10000} + API_KEEP_ALIVE_TIMEOUT_MS: ${API_KEEP_ALIVE_TIMEOUT_MS:-5000} + API_SOCKET_TIMEOUT_MS: ${API_SOCKET_TIMEOUT_MS:-15000} + API_TLS_HANDSHAKE_TIMEOUT_MS: ${API_TLS_HANDSHAKE_TIMEOUT_MS:-3000} + API_LISTEN_BACKLOG: ${API_LISTEN_BACKLOG:-1024} + API_NODE_ARGS: ${API_NODE_ARGS:---max-old-space-size=512} + API_MAX_MEMORY_RESTART: ${API_MAX_MEMORY_RESTART:-768M} + API_KILL_TIMEOUT_MS: ${API_KILL_TIMEOUT_MS:-5000} + API_RESTART_DELAY_MS: ${API_RESTART_DELAY_MS:-2000} PM2_ENABLED: ${PM2_ENABLED:-true} STRATUM_WORKERS: ${STRATUM_WORKERS:-2} STRATUM_PORTS: ${STRATUM_PORTS:-3333,3332,3331,3330} diff --git a/docker-compose.yml b/docker-compose.yml index 79f1b89..c02a03e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,6 +79,17 @@ services: REDIS_URL: redis://redis:6379 API_PORT: ${API_PORT:-3334} API_SECURE: ${API_SECURE:-false} + API_MAX_CONNECTIONS: ${API_MAX_CONNECTIONS:-512} + API_REQUEST_TIMEOUT_MS: ${API_REQUEST_TIMEOUT_MS:-15000} + API_HEADERS_TIMEOUT_MS: ${API_HEADERS_TIMEOUT_MS:-10000} + API_KEEP_ALIVE_TIMEOUT_MS: ${API_KEEP_ALIVE_TIMEOUT_MS:-5000} + API_SOCKET_TIMEOUT_MS: ${API_SOCKET_TIMEOUT_MS:-15000} + API_TLS_HANDSHAKE_TIMEOUT_MS: ${API_TLS_HANDSHAKE_TIMEOUT_MS:-3000} + API_LISTEN_BACKLOG: ${API_LISTEN_BACKLOG:-1024} + API_NODE_ARGS: ${API_NODE_ARGS:---max-old-space-size=512} + API_MAX_MEMORY_RESTART: ${API_MAX_MEMORY_RESTART:-768M} + API_KILL_TIMEOUT_MS: ${API_KILL_TIMEOUT_MS:-5000} + API_RESTART_DELAY_MS: ${API_RESTART_DELAY_MS:-2000} PM2_ENABLED: ${PM2_ENABLED:-true} STRATUM_WORKERS: ${STRATUM_WORKERS:-2} STRATUM_PORTS: ${STRATUM_PORTS:-3333,3332,3331,3330} diff --git a/ecosystem.config.js b/ecosystem.config.js index 569b083..38919ef 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,3 +1,10 @@ +const apiNodeArgs = (process.env.API_NODE_ARGS || '--max-old-space-size=512') + .split(/\s+/) + .filter(arg => arg.length > 0); +const apiMaxMemoryRestart = process.env.API_MAX_MEMORY_RESTART || '768M'; +const apiKillTimeout = parseInt(process.env.API_KILL_TIMEOUT_MS || '5000', 10); +const apiRestartDelay = parseInt(process.env.API_RESTART_DELAY_MS || '2000', 10); + module.exports = { apps: [ // API instance @@ -6,6 +13,14 @@ module.exports = { script: './dist/main.js', instances: 1, exec_mode: 'fork', + node_args: apiNodeArgs, + max_memory_restart: apiMaxMemoryRestart, + kill_timeout: apiKillTimeout, + restart_delay: apiRestartDelay, + min_uptime: '10s', + max_restarts: 10, + pmx: false, + vizion: false, env: { MASTER: 'false', API_ONLY: 'true', diff --git a/full-setup/docker-compose-mainnet.yml b/full-setup/docker-compose-mainnet.yml index dd461cd..baaac49 100644 --- a/full-setup/docker-compose-mainnet.yml +++ b/full-setup/docker-compose-mainnet.yml @@ -97,6 +97,19 @@ services: DB_PASSWORD: public_pool DB_DATABASE: public_pool_mainnet REDIS_URL: redis://redis:6379 + API_PORT: ${API_PORT:-3334} + API_SECURE: ${API_SECURE:-false} + API_MAX_CONNECTIONS: ${API_MAX_CONNECTIONS:-512} + API_REQUEST_TIMEOUT_MS: ${API_REQUEST_TIMEOUT_MS:-15000} + API_HEADERS_TIMEOUT_MS: ${API_HEADERS_TIMEOUT_MS:-10000} + API_KEEP_ALIVE_TIMEOUT_MS: ${API_KEEP_ALIVE_TIMEOUT_MS:-5000} + API_SOCKET_TIMEOUT_MS: ${API_SOCKET_TIMEOUT_MS:-15000} + API_TLS_HANDSHAKE_TIMEOUT_MS: ${API_TLS_HANDSHAKE_TIMEOUT_MS:-3000} + API_LISTEN_BACKLOG: ${API_LISTEN_BACKLOG:-1024} + API_NODE_ARGS: ${API_NODE_ARGS:---max-old-space-size=512} + API_MAX_MEMORY_RESTART: ${API_MAX_MEMORY_RESTART:-768M} + API_KILL_TIMEOUT_MS: ${API_KILL_TIMEOUT_MS:-5000} + API_RESTART_DELAY_MS: ${API_RESTART_DELAY_MS:-2000} PM2_ENABLED: "true" STRATUM_WORKERS: ${STRATUM_WORKERS:-2} diff --git a/src/main.ts b/src/main.ts index 048749c..ab9c58a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,14 @@ import * as ecc from 'tiny-secp256k1'; import { AppModule } from './app.module'; +const DEFAULT_API_MAX_CONNECTIONS = 512; +const DEFAULT_API_REQUEST_TIMEOUT_MS = 15000; +const DEFAULT_API_HEADERS_TIMEOUT_MS = 10000; +const DEFAULT_API_KEEP_ALIVE_TIMEOUT_MS = 5000; +const DEFAULT_API_SOCKET_TIMEOUT_MS = 15000; +const DEFAULT_API_TLS_HANDSHAKE_TIMEOUT_MS = 3000; +const DEFAULT_API_LISTEN_BACKLOG = 1024; + async function bootstrap() { if (process.env.API_PORT == null) { console.error('It appears your environment is not configured, create and populate an .env file.'); @@ -28,6 +36,7 @@ async function bootstrap() { https: { key: readFileSync(keyPath), cert: readFileSync(certPath), + handshakeTimeout: getPositiveIntegerEnv('API_TLS_HANDSHAKE_TIMEOUT_MS', DEFAULT_API_TLS_HANDSHAKE_TIMEOUT_MS), } }; } @@ -65,13 +74,20 @@ async function bootstrap() { return; } - await app.listen(process.env.API_PORT, '0.0.0.0', (err, address) => { - if (err) { - console.error(err); - process.exit(1); - } + configureApiServer(app.getHttpServer()); + + try { + const address = await app.listen({ + port: parseInt(process.env.API_PORT, 10), + host: '0.0.0.0', + backlog: getPositiveIntegerEnv('API_LISTEN_BACKLOG', DEFAULT_API_LISTEN_BACKLOG), + }); console.log(`API listening on ${address}`); - }); + } catch (error) { + console.error('API listen failed:', error); + await app.close().catch(() => undefined); + process.exit(1); + } // --- Live-reload TLS certs/keys when they change on disk --- if (secure) { @@ -111,4 +127,26 @@ async function bootstrap() { } } +function configureApiServer(server: any) { + const socketTimeoutMs = getPositiveIntegerEnv('API_SOCKET_TIMEOUT_MS', DEFAULT_API_SOCKET_TIMEOUT_MS); + + server.maxConnections = getPositiveIntegerEnv('API_MAX_CONNECTIONS', DEFAULT_API_MAX_CONNECTIONS); + server.requestTimeout = getPositiveIntegerEnv('API_REQUEST_TIMEOUT_MS', DEFAULT_API_REQUEST_TIMEOUT_MS); + server.headersTimeout = getPositiveIntegerEnv('API_HEADERS_TIMEOUT_MS', DEFAULT_API_HEADERS_TIMEOUT_MS); + server.keepAliveTimeout = getPositiveIntegerEnv('API_KEEP_ALIVE_TIMEOUT_MS', DEFAULT_API_KEEP_ALIVE_TIMEOUT_MS); + server.timeout = socketTimeoutMs; + + server.on('connection', (socket: NodeJS.ReadWriteStream & { setTimeout?: (ms: number) => void; destroy?: () => void }) => { + socket.setTimeout?.(socketTimeoutMs); + socket.once?.('timeout', () => { + socket.destroy?.(); + }); + }); +} + +function getPositiveIntegerEnv(name: string, fallback: number) { + const value = Number(process.env[name]); + return Number.isInteger(value) && value > 0 ? value : fallback; +} + bootstrap();