Performance Tuning#
Vulnerability-Lookup is a multi-layered system. Tuning each layer appropriately is important for production deployments handling large volumes of vulnerability data.
This guide covers the main components: Gunicorn (web server), Kvrocks (primary storage), Valkey/Redis (cache), PostgreSQL (user accounts and metadata), and optional PgBouncer (connection pooling).
Gunicorn (Web Server)#
Gunicorn serves the Flask application. Its settings are controlled via
config/generic.json and applied in bin/start_website.py.
Workers#
The website_workers setting in config/generic.json controls the number of
Gunicorn worker processes. A common starting point:
workers = (2 × CPU_CORES) + 1
For I/O-bound workloads (typical for Vulnerability-Lookup), you can go higher since the
gevent async worker class is used. Monitor memory usage — each worker is a separate
process.
Timeouts#
The default request timeout is 300 seconds (5 minutes). This accommodates large API responses (e.g., full vulnerability exports). The graceful shutdown timeout is 2 seconds.
Key Gunicorn flags used:
--worker-class gevent # async worker for concurrent I/O
--timeout 300 # max request processing time
--graceful-timeout 2 # grace period on shutdown
--reuse-port # SO_REUSEPORT for load balancing across workers
--proxy-protocol # preserve client IPs behind a reverse proxy
Kvrocks (Primary Storage)#
Kvrocks is the primary data store for all vulnerability records. Its configuration
is in storage/kvrocks.conf.
Worker Threads#
workers 8
The number of I/O threads processing client commands. Increase on machines with more CPU cores and high concurrency. A reasonable value is the number of CPU cores available to the Kvrocks process.
Connection Limits#
maxclients 10000
tcp-backlog 511
maxclients sets the maximum number of concurrent client connections. Ensure the
OS file descriptor limit (ulimit -n) is higher than this value.
For tcp-backlog, also verify the kernel setting:
sysctl net.core.somaxconn # should be >= tcp-backlog
sysctl net.ipv4.tcp_max_syn_backlog
RocksDB Tuning#
Kvrocks uses RocksDB as its storage engine. Key parameters:
rocksdb.block_cache_size 4096 # block cache in MB (default 4 GB)
rocksdb.max_open_files 8096 # file descriptors for SST files
rocksdb.write_buffer_size 64 # memtable size in MB
rocksdb.max_write_buffer_number 4 # concurrent memtables
rocksdb.max_background_jobs 4 # compaction and flush threads
rocksdb.target_file_size_base 128 # SST file target size in MB
rocksdb.max_total_wal_size 512 # WAL size limit in MB
Recommendations for large instances:
block_cache_size: Increase to fit the hot dataset in memory. On a dedicated server with plenty of RAM, allocate 25-50% of available memory.
max_open_files: Set to
-1to keep all files open (avoids open/close overhead).write_buffer_size: Increase for write-heavy workloads (e.g., initial bulk import). Larger buffers reduce write amplification.
max_background_jobs: Increase on multi-core systems to speed up compaction.
Disk I/O Throttling#
max-io-mb 0 # 0 = no limit on flush/compaction write rate
max-replication-mb 0 # 0 = no limit on replication rate
Set max-io-mb to a non-zero value if compaction I/O impacts foreground latency.
Valkey/Redis (Cache Layer)#
The cache layer uses a Unix domain socket for lowest-latency local communication.
Its configuration is in cache/cache.conf.
port 0 # TCP disabled — Unix socket only
unixsocket cache.sock
unixsocketperm 700
tcp-keepalive 300
Since the cache runs locally on a Unix socket (no TCP overhead), it is already optimized for latency. The main tuning concern is memory:
Monitor memory usage with
redis-cli -s cache/cache.sock INFO memory.Set
maxmemoryand an eviction policy (e.g.,allkeys-lru) if the cache should not grow unbounded.
PostgreSQL#
PostgreSQL stores user accounts, comments, bundles, sightings, and watchlists. Tune based on your expected user base and concurrent connections.
# Connection / memory
max_connections = 200 # keep moderate when using PgBouncer
shared_buffers = 64GB # ~25% of RAM
work_mem = 64MB # per query memory; adjust if heavy queries
maintenance_work_mem = 4GB
# WAL / checkpoints
wal_buffers = 16MB
checkpoint_completion_target = 0.9
max_wal_size = 4GB
min_wal_size = 1GB
# Autovacuum
autovacuum = on
autovacuum_max_workers = 10
autovacuum_naptime = 10s
autovacuum_vacuum_cost_limit = 2000
# Logging
log_connections = on
log_disconnections = on
log_min_duration_statement = 2000 # log slow queries > 2s
Note
The shared_buffers value should be adjusted based on your actual server RAM.
A common guideline is ~25% of available memory.
PgBouncer (Optional)#
PgBouncer is a connection pooler that sits between the application and PostgreSQL. It reduces the overhead of establishing new connections and manages a pool of persistent connections. This is recommended when running many Gunicorn workers (each worker may hold its own database connections).
[databases]
vulnlookup = host=127.0.0.1 port=5432 dbname=vulnlookup
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
admin_users = vuln-lookup
pool_mode = transaction # best for web apps
default_pool_size = 200 # number of server connections per DB
min_pool_size = 50
reserve_pool_size = 50
reserve_pool_timeout = 5.0 # seconds to wait for reserve
max_client_conn = 5000
# Logging / stats
logfile = /var/log/pgbouncer/pgbouncer.log
log_connections = 1
log_disconnections = 1
Note
When using PgBouncer, point DB_CONFIG_DICT in config/website.py to the
PgBouncer port (6432) instead of PostgreSQL directly (5432).
Note
Here default_pool_size (200) equals PostgreSQL’s max_connections (200).
Consider reserving a few connections for direct admin access by either increasing
max_connections or slightly reducing default_pool_size.
SQLAlchemy Connection Pool#
The SQLAlchemy engine options in config/website.py control the application-level
connection pool to PostgreSQL:
Without PgBouncer (default):
SQLALCHEMY_ENGINE_OPTIONS = {
"pool_size": 100, # persistent connections in the pool
"max_overflow": 50, # extra connections during traffic spikes
"pool_timeout": 30, # seconds to wait for a connection
"pool_recycle": 1800, # recycle connections every 30 minutes
"pool_pre_ping": True, # verify connection liveness before use
}
Warning
Each Gunicorn worker process creates its own connection pool. The total number of
possible connections is pool_size × website_workers. For example, with
pool_size=100 and 49 workers, the application could open up to 4,900 connections,
far exceeding PostgreSQL’s max_connections. Without PgBouncer, reduce pool_size
so that pool_size × website_workers stays well below max_connections.
With PgBouncer (let PgBouncer manage the pool):
SQLALCHEMY_ENGINE_OPTIONS = {
"pool_size": 0, # no persistent connections; rely on PgBouncer
"max_overflow": 0,
"pool_pre_ping": True,
"pool_timeout": 30,
"pool_recycle": 3600,
}
Logging#
Reducing log verbosity in production decreases disk I/O and improves throughput:
config/generic.json: setlogleveltoWARNINGorERRORconfig/website.py: setLOG_LEVELtoWARNINGstorage/kvrocks.conf:log-level warning(default)Per-feeder log levels in
config/modules.cfg: changelevel = DEBUGtolevel = WARNINGfor production
The logging configuration in config/logging.json uses rotating file handlers
(1 MB per file, 5 backups) which bounds disk usage.
Operating System Tuning#
File Descriptors#
Kvrocks, Valkey, and Gunicorn all benefit from high file descriptor limits:
# /etc/security/limits.conf (or systemd unit override)
* soft nofile 65536
* hard nofile 65536
Network Stack#
For high-concurrency deployments:
sysctl -w net.core.somaxconn=4096
sysctl -w net.ipv4.tcp_max_syn_backlog=4096
sysctl -w net.core.netdev_max_backlog=4096
sysctl -w vm.overcommit_memory=1 # recommended for Redis/Kvrocks
Transparent Huge Pages (THP)#
Disable THP for Kvrocks and Valkey to avoid latency spikes:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
Reverse Proxy (Nginx)#
When running behind Nginx, a minimal performance-oriented configuration:
upstream vulnlookup {
server 127.0.0.1:10001;
keepalive 64;
}
server {
listen 443 ssl http2;
server_name vulnerability.example.org;
client_max_body_size 10m;
location / {
proxy_pass http://vulnlookup;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10;
proxy_read_timeout 300; # match Gunicorn timeout
proxy_send_timeout 300;
}
}
Note
Gunicorn is started with --proxy-protocol. If your reverse proxy supports
the PROXY protocol, enable it. Otherwise, rely on X-Forwarded-For headers
and consider removing --proxy-protocol from bin/start_website.py.
Summary#
Layer |
Key Settings |
Tune When |
|---|---|---|
Gunicorn |
|
High concurrent users, slow responses |
Kvrocks |
|
Large dataset, slow queries, high write load |
Valkey/Redis |
|
Cache memory growing unbounded |
PostgreSQL |
|
Slow user/account queries, high concurrency |
PgBouncer |
|
Many workers exhausting PostgreSQL connections |
SQLAlchemy |
|
Connection errors, stale connections |
OS |
|
Connection refused errors, latency spikes |