ELT memory allocation on one EPYC server
A proposed 512GB reusable ELT pool on a 768GB EPYC server. The data-platform design defines In-Memory, Arrow IPC and Parquet paths with execution limits.
Proposed execution policy
I designed a memory policy for an ELT stack using Dagster, Trino, DataFusion and PostgreSQL. The assumed server has AMD EPYC 9175F 16c/32t, 768GB DDR5-6400, RTX 6000 Pro Max-Q and NVMe 3.84TB. The plan reserves 512GB for ELT and reuses it step by step.
The wider data-platform idea supports local, offline-first flows such as Airbyte -> Iceberg / Trino -> visualization, with possible 48h Prototype and data-platform delivery formats. This is the proposed execution policy for that design.
Separating memory peaks
Even with 768GB, concurrent Trino queries, DataFusion transforms, PostgreSQL loads and Dagster workers can overlap in memory. The policy must account for OOM, swap and restarting partially failed work.
The proposed rule is:
Reuse a dedicated
512GBpool step by step, keep light paths inIn-Memory, and fall back toArrow IPCorParquetwhen the workload gets large.
It defines both memory use and how failed work can resume.
Base Resource Assumptions
These are the hardware assumptions:
- Server:
AMD EPYC 9175F 16c/32t (5GHz) + 768GB DDR5-6400 + RTX 6000 Pro Max-Q + NVMe 3.84TB - ELT pool: a dedicated
512GBreserved for workflow execution - Bandwidth: memory bandwidth around
~600GB/s, NVMe I/O in multipleGB/s, network at10GbE
The 512GB pool is assigned to the active ELT step. Workload budgets remain explicit as ingestion, monitoring, cataloging and visualization services are added.
Memory Operation Policy
Principle
The memory policy is simple on purpose:
- Reuse the
512GBpool step by step - Prefer direct
In-Memoryhandoff when everything stays in one process - If the work is split across processes or containers, keep the combined usage within the same
512GBceiling
Heavy analytics steps run one at a time, release memory when finished and let the next step reuse the pool.
Switching Thresholds
I also wrote down explicit thresholds for intermediate data size:
| Intermediate size | Recommended mode | Notes |
|---|---|---|
<= 40-50% (200-250GB) | In-Memory (in-process + InMemoryIOManager) | Fastest path because it directly uses ~600GB/s memory bandwidth |
50-80% | Arrow IPC stream | Good compromise with lower overhead and NVMe-backed safety |
>80% or concurrent heavy ops | Parquet temporary output | Better restart behavior and stronger safety margin |
These thresholds determine whether intermediate data stays in memory or is written to Parquet.
Optimization by Component
DataFusion (Rust)
On the DataFusion side, the biggest rule is to avoid accidentally materializing huge intermediates too early.
- Set
MemoryPoollimits per op usingS/M/Lprofiles - Point
DiskManagerat NVMe under/opt/tmpso forced spill is available - Prefer the streaming API, especially
execute_stream() - Align partition count with the physical core count (
16c)
Streaming avoids collecting intermediates that do not need to be materialized.
Trino
Trino needs both internal limits and external scheduling discipline.
JVM: -Xmx192g
max-spill-per-node=512GB
query.max-memory-per-node=128GB
query.max-total-memory-per-node=160GB
Trino uses a 256GB container with NVMe spill. Dagster allows only one heavy query at a time.
PostgreSQL
PostgreSQL handles persistence and bulk loading.
shared_buffers=8-16GB
work_mem=128MB
maintenance_work_mem=2-4GB
wal_compression=lz4
checkpoint_timeout=20-30min
max_wal_size=12-16GB
The preferred load patterns are:
UNLOGGED TABLE -> COPY -> build indexes -> LOGGEDCTAS (CREATE TABLE AS SELECT)
synchronous_commit=off remains an option only during loading.
Dagster
Dagster selects the executor by workload size.
- Light jobs:
in_process_executor + InMemoryIOManager - Heavy jobs:
multiprocess_executor + per-op Podman containers (--memory/--cpuset-cpus)
Then I tag workloads to control concurrency:
- heavy operations (
Trino, largeDataFusionops):max_concurrent=1 - light operations such as metadata handling,
dbt run, or logging: parallel execution is allowed
The same limits apply across jobs.
Process / Container Design
In-Memory (fastest path)
The memory path uses in-process executor and shallow-copy handoffs of pyarrow.Table or RecordBatch. Its proposed limit is about <=200GB, or roughly 50% of the ELT pool.
Beyond that size, downstream op startup must be included in the memory peak.
Container isolation per op
Each op can run with podman run --memory=... --cpuset-cpus=.... Process exit releases its memory for the next step in the 512GB pool.
This follows the wider plan to reproduce local workloads through Podman.
Release memory before child startup
If Python launches Rust child processes while the parent is still holding large buffers, combined peak memory becomes the real problem. A few specific safeguards:
- release parent buffers with
del - let scopes end before child startup
- call
gc.collect() - prefer Python
spawnoverfork
Losing fork Copy-on-Write can increase physical memory use.
I/O Optimization
Intermediate formats and storage locations are assigned as follows.
Arrow IPC: a fast spill path, close to zero-copyParquet: the safer fallback withzstd(1-3)and row groups at128-256MBNVMe: centralize spill directories under/opt/tmp/dev/shm: increase to--shm-size 2-4GBfor Arrow Plasma / IPC- cleanup: remove temporary files at job end, including failure paths
Arrow IPC prioritizes transfer speed; Parquet prioritizes saved state and retries.
Operating Rules
The execution rules are:
- Run heavy queries one at a time.
- End each op, free its memory, and reuse the same
512GBpool. - Switch between
In-Memory,Arrow IPC, andParquetbased on intermediate size. - Use
Parquetwhen restartability matters most. - Use
In-Memoryshallow-copy or pointer-style transfer when raw speed matters most.
Different ELT jobs use the same criteria.
Expected benefits
The intended benefits are:
- Speed: use the
~600GB/smemory bandwidth throughIn-Memoryhandoffs. - Stability: limit peaks with cgroups, spill and step-wise reuse.
- Flexibility: choose
Arrow IPCorParquetby intermediate size. - Execution control: change modes with the
Dagsterexecutor.
These are design goals. Concurrency limits keep memory peaks within the single-server budget.
Summary
The plan reuses 512GB per step, keeps smaller workloads in In-Memory, and saves larger intermediates through Arrow or Parquet. Container exit releases op memory.
Configuration changes planned
The next planned step is to define podman-compose.yml CPU and memory profiles and Dagster executor settings. The surrounding Airbyte OSS, Great Expectations, OpenMetadata and MLflow services would follow the same policy.
