Database Tuning
You have a PostgreSQL server for IdentityScribe. These settings size memory, storage, and WAL for IdentityScribe’s workload.
For caller-side search configuration guidance, see Query performance.
All formulas assume a dedicated database server. If PostgreSQL shares a host with IdentityScribe, reduce memory fractions accordingly.
Workload profile
Section titled “Workload profile”IdentityScribe’s database workload has distinct characteristics that inform tuning:
- Three isolated connection pools — a default total of ~43 connections serving different workloads: write-heavy transcription tasks, schema maintenance, and latency-sensitive query serving (LDAP, REST, GraphQL).
- Search data distributed across many storage objects — larger directories spread hot data across entry types and attributes, so cache sizing matters more than one table’s size.
- Several acceleration structures per searchable attribute — equality, ordering, range, and substring support can consume substantial storage; substring-search acceleration is usually the largest part.
- Read-dominant steady state — equality lookups, range scans with cursor pagination, substring search, and sorted result sets. All served concurrently from LDAP, REST, and GraphQL channels.
- Bursty writes — event-sourced single-row inserts at steady state; multi-row bulk writes during reconciliation. Preparing a new entry type also builds its search acceleration data in the background.
Recommended settings
Section titled “Recommended settings”Settings are parameterized by server RAM and CPU cores.
Memory
Section titled “Memory”| Setting | Formula | 16 GB | 32 GB | 64 GB | Rationale |
|---|---|---|---|---|---|
shared_buffers | RAM × 0.25 | 4 GB | 8 GB | 16 GB | Must hold hot search data. Undersizing causes buffer churn — every query evicts pages needed by the next |
effective_cache_size | RAM × 0.75 | 12 GB | 24 GB | 48 GB | Reflects OS page cache availability so PostgreSQL can favor index-backed reads when hot data fits in cache |
work_mem | (RAM × 0.25) / (connections × 4) | 16 MB | 32 MB | 64 MB | Per memory-consuming operation. Too high risks OOM under load; too low forces disk work on sorted pagination queries |
maintenance_work_mem | min(RAM × 0.05, 2 GB) | 800 MB | 1.6 GB | 2 GB | Background search-data preparation is memory-intensive. Directly affects how fast new entry types become available |
Session-level work_mem override
Section titled “Session-level work_mem override”The table above recommends server-level work_mem for postgresql.conf. IdentityScribe also sets work_mem = 16MB per query connection via database.connection-hints.session-flags.work-mem. This connection-level setting overrides the server default for IdentityScribe’s connections.
16MB works for most deployments. If query performance degrades over time or under load, insufficient work_mem can cause PostgreSQL to spill sort and hash operations to disk — which is orders of magnitude slower than in-memory processing.
Observe detects spills automatically. When it finds disk spilling, it recommends a work_mem value and provides a ready-to-use config snippet. You can also calculate manually:
Tuning formula
Section titled “Tuning formula”A safe starting point reserves 25% of server RAM for all concurrent work_mem allocations. Estimate how many sort,
hash, bitmap, and parallel-worker allocations one busy query may use; four is a conservative starting point:
work_mem_budget_mb = total_ram_mb * 0.25work_mem_ceiling_mb = min(512, work_mem_budget_mb / (active_query_connections * memory_nodes_per_query))work_mem = largest standard size at or below work_mem_ceiling_mbFor a server with 64GB RAM, the default 43 pooled connections, and four concurrent allocations per query:
work_mem_budget_mb = 65536 * 0.25 = 16384MBwork_mem_ceiling_mb = 16384 / (43 * 4) ≈ 95MBwork_mem = 64MBaggregate ceiling = 64MB * 43 * 4 = 11008MB, below the 16384MB budgetUse a standard size at or below the ceiling (16, 32, 64, 96, 128, 256, or 512 MB). If the calculated ceiling is below 16MB, reduce active query concurrency or allocate a larger budget only after accounting for all other server memory. Monitor temporary-file counts after each increase; if they stop growing, you have enough.
How to adjust
Section titled “How to adjust”Override per-channel or globally in IdentityScribe config:
database.connection-hints.session-flags { work-mem = "64MB"}Or per-channel (e.g., only for REST API queries):
channels.rest.connection-hints.session-flags { work-mem = "128MB"}The value must match <number><unit> where unit is B, kB, MB, GB, or TB (e.g., "256MB", "1GB").
Raising work_mem increases memory pressure. A single query can use work_mem multiple times — once per sort or hash operation, and once per parallel worker. On a server running many concurrent queries, doubling work_mem across the board can trigger out-of-memory errors. Raise gradually and verify in Observe that temporary-file pressure falls before moving higher.
Storage
Section titled “Storage”| Setting | Value | Rationale |
|---|---|---|
random_page_cost | 1.1 | SSD random reads are nearly as fast as sequential. Helps PostgreSQL favor accelerated reads for selective searches |
effective_io_concurrency | 200 | SSD can handle many concurrent read requests and keep large searches moving |
For HDD storage, use random_page_cost = 2.0 and effective_io_concurrency = 4.
Parallelism
Section titled “Parallelism”| Setting | Formula | 4 cores | 8 cores | 16 cores |
|---|---|---|---|---|
max_parallel_workers_per_gather | cores / 4 | 1 | 2 | 4 |
max_parallel_maintenance_workers | cores / 2 | 2 | 4 | 4 |
max_parallel_workers | cores / 2 | 2 | 4 | 8 |
IdentityScribe benefits from parallel query execution for large equality searches, where PostgreSQL can share work across worker processes.
Maintenance parallelism is higher to speed up index creation when new entry types are prepared.
Partition-wise operations
Section titled “Partition-wise operations”| Setting | Value | Rationale |
|---|---|---|
enable_partitionwise_join | off | Shipped IdentityScribe default; keep it unless support-backed measurements for this deployment select otherwise |
enable_partitionwise_aggregate | on | Lets PostgreSQL combine partitioned results efficiently for sorted and paged searches |
IdentityScribe applies these values on each query connection, so no server-wide change is required.
Do not enable partition-wise joins server-wide for IdentityScribe without a deployment-specific benchmark and support review. Other applications on the same PostgreSQL server may need different values.
WAL and checkpoints
Section titled “WAL and checkpoints”| Setting | Value | Rationale |
|---|---|---|
wal_level | replica | Required — event sourcing requires WAL for crash recovery and potential replication |
synchronous_commit | on | Required — event sourcing demands durable writes. Losing committed events breaks sync state |
max_wal_size | RAM × 0.125 (2–8 GB) | Larger WAL before forced checkpoint. Sync writes are bursty during reconciliation |
checkpoint_timeout | 10–15 min | Reduces checkpoint frequency. Default 5 min causes excessive I/O during sustained writes |
checkpoint_completion_target | 0.9 | Spread checkpoint writes over 90% of the interval (default; keep as-is) |
Connections
Section titled “Connections”| Setting | Formula | Rationale |
|---|---|---|
max_connections | pool total + 15 | Sum of all connection pools, plus headroom for admin, monitoring, and migrations. Default pool total is ~43, so 60 is a safe starting point |
Autovacuum
Section titled “Autovacuum”| Setting | Value | Rationale |
|---|---|---|
autovacuum | on | Required — partitioned tables with frequent updates need regular vacuum to prevent bloat and maintain visibility maps |
autovacuum_vacuum_scale_factor | 0.05 | More aggressive than default (0.2) — partitions are smaller, so the default leaves too many dead tuples proportionally |
autovacuum_analyze_scale_factor | 0.02 | Keeps statistics fresh. Stale partition stats reduce pruning efficiency and can slow filter-heavy searches |
Settings that must not be used in production
Section titled “Settings that must not be used in production”Example configuration
Section titled “Example configuration”For a 32 GB RAM / 8-core SSD server with default IdentityScribe pool sizes:
# Memoryshared_buffers = 8GBeffective_cache_size = 24GBwork_mem = 32MBmaintenance_work_mem = 1536MB
# Storage (SSD)random_page_cost = 1.1effective_io_concurrency = 200
# Parallelismmax_parallel_workers_per_gather = 2max_parallel_maintenance_workers = 4max_parallel_workers = 4
# IdentityScribe query defaults (pin server-wide only after deployment-specific review)enable_partitionwise_join = offenable_partitionwise_aggregate = on
# WAL and Checkpointsmax_wal_size = 4GBcheckpoint_timeout = 10min
# Connectionsmax_connections = 60
# Autovacuum (tuned for partitioned tables)autovacuum_vacuum_scale_factor = 0.05autovacuum_analyze_scale_factor = 0.02After restarting PostgreSQL, confirm buffer allocation with SHOW shared_buffers and monitor query performance on the Health and Monitoring dashboard.
IdentityScribe connection pools
Section titled “IdentityScribe connection pools”IdentityScribe manages three separate database connection pools to isolate workloads. These are configured in the IdentityScribe config (not postgresql.conf).
| Pool | Used by | Default size | Tuning guidance |
|---|---|---|---|
| Batch | Transcription tasks (main write workload) | concurrency + 5, clamped to concurrency × 1.5 | Increase if scribe_db_connections_pending is consistently high and traces point at write work; decrease on memory-constrained hosts |
| System | Migrations, maintenance, health checks, DDL | max(transcribeCount + 4, concurrency / 4), clamped [2, max-pool-size / 2] | Increase if maintenance windows overlap with heavy write load |
| Channel | REST, GraphQL, LDAP query serving | concurrency, minimum 2 | Increase if channel latency is dominated by connection acquisition waits (scribe_db_connections_pending with high query permit pressure) |
The total connection count is the sum of all three pools. Ensure max_connections in postgresql.conf can accommodate the total (see the Connections table above).
A semaphore (default: channel pool size) caps concurrent query connections across all channels. HTTP and GraphQL queries that exceed this wait up to query-http-acquisition-timeout (default: 5s) and return 503 with a Retry-After header. LDAP queries block up to their query time limit instead.
| Config | Env var | Default |
|---|---|---|
database.max-pool-size | SCRIBE_DATABASE_MAX_POOL_SIZE | concurrency + 5 |
database.system-pool-size | SCRIBE_DATABASE_SYSTEM_POOL_SIZE | max(transcribeCount + 4, concurrency / 4) |
database.channel-pool-size | SCRIBE_DATABASE_CHANNEL_POOL_SIZE | concurrency |
database.query-http-acquisition-timeout | SCRIBE_DATABASE_QUERY_HTTP_ACQUISITION_TIMEOUT | 5s |
Adaptive filtered-search tuning
Section titled “Adaptive filtered-search tuning”For filtered, sorted, prefix, and substring searches, IdentityScribe adjusts execution automatically based on live directory size, attribute distribution, and recent production traffic.
- Narrow-match searches finish quickly when only a small share of rows match.
- Sorted searches that narrow to a small set of entries finish quickly even over large directories — a sorted or virtual-list-view browse selected by entry distinguished names (a single one or a long list), a unique identifier (
uuid/uoid), or a selective attribute value applies that narrowing before sorting, instead of ordering the whole result set first. - Broad-match searches stay predictable when a large share of rows match.
- Prefix searches reuse recent measurements for repeated patterns.
- Range searches use maintained coverage data when that is expected to reduce work.
No manual tuning is required for standard deployments. The default behaviour is conservative during startup and becomes more specific as normal traffic provides evidence.
When to intervene
Section titled “When to intervene”Leave adaptive search tuning enabled unless you see a repeatable regression on a specific attribute or search pattern. If that happens:
- Capture a Query Diagnostic Report for the affected request.
- Compare the affected request against the Health and Monitoring dashboard for database load, queueing, and timeout signals.
- Contact Kenoxa support with the report and the time window. Support can provide deployment-specific overrides when a temporary safety valve is needed.
Do not tune the advanced search thresholds from symptoms alone. The same latency symptom can come from stale database statistics, undersized memory, connection pressure, or an attribute distribution that changed after a bulk import.
Statistics target
Section titled “Statistics target”IdentityScribe dynamically computes the PostgreSQL statistics target before each maintenance refresh, adjusting it based on directory size. This keeps filter and sort performance more consistent at scale without the overhead of a blanket high target.
The computed target is logged at the start of each maintenance window.
| Config | Env var | Default |
|---|---|---|
database.maintenance.statistics-target-ratio | SCRIBE_DATABASE_MAINTENANCE_STATISTICS_TARGET_RATIO | 0.00005 |
Set to 0 to disable dynamic tuning and use PostgreSQL’s default.
Sort index backfill
Section titled “Sort index backfill”Ordering support data is built in the background at startup; the service remains fully operational during the build window.
Backfill concurrency auto-scales from available system pool resources, reserving enough connections for maintenance and health checks.
| Config | Env var | Default | When to change |
|---|---|---|---|
database.sort-index-backfill.max-concurrent | SCRIBE_DATABASE_SORT_INDEX_BACKFILL_MAX_CONCURRENT | Auto-scaled | Increase to shorten backfill window on high-spec systems; decrease to reduce read pressure during initial sync |
Quick reference
Section titled “Quick reference”Common IdentityScribe database configuration environment variables:
| Env var | Default |
|---|---|
SCRIBE_DATABASE_MAX_POOL_SIZE | concurrency + 5 |
SCRIBE_DATABASE_SYSTEM_POOL_SIZE | max(transcribeCount + 4, concurrency / 4) |
SCRIBE_DATABASE_CHANNEL_POOL_SIZE | concurrency |
SCRIBE_DATABASE_QUERY_HTTP_ACQUISITION_TIMEOUT | 5s |
SCRIBE_DATABASE_MAINTENANCE_STATISTICS_TARGET_RATIO | 0.00005 |
SCRIBE_DATABASE_SORT_INDEX_BACKFILL_MAX_CONCURRENT | Auto-scaled |