You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Transition to efficient background processing #3551
The dstack server main bottleneck for supporting more active workloads is the implementation of background processing. Background workers hold DB transactions active while processing, which makes DB connections a scarce resource.
The background processing tasks need to be re-implemented so that long running operations are never performed while inside a DB transaction. The main idea is to commit locks to the DB. Here's a detailed PoC for this approach: https://github.com/r4victor/dbqueue_poc
On a related note, background worker execution times should be optimized since ultimately the processing throughput is proportional to the execution time. The longest core task is process_submitted_jobs, so it should be profiled and optimized first.
Benchmarking old tasks vs pipelines (one server replica, SQLite):
Provisioning N instances (pending => idle, non-cluster fleet, AWS):
N=1: 1m:10s => 45s
N=50: ~4m => 1m:30s
N=200: ~11 m => ~3-4m.
Terminating N instances (idle => terminated, AWS):
N=1: 9s => 3s
N=50: 1m => 9s
N=200: no time => 30s
Running N-nodes task on a provisioned fleet (submitted => running / any job running, first time):
N=1: 1m:20s => 1m:25s
N=50: 3m:40s => 3m:40s
N=200: >25m => 4m:30s
Running N-nodes task on a provisioned fleet (submitted => running / any job running, second time, no pull):
N=1: 50s => 50s
N=50: 3m:40s => 2m
N=200: no time => 4m:30s
Terminating N-nodes task (graceful stop):
N=1: 25s => 35s
N=50: ~3m => 35s
N=200: no time => 45s
Running N 1-job runs on a provisioned fleet (submitted => running, includes time to accept N apply requests, 200 idle instances):
N=10: >6m => 1m:25s
N=50: no time => 1m:30s
N=200: no time => 4m
Peak memory usage (heap):
10 instances and 10 jobs: 521MB => 532MB
100 instances and 100 jobs 598MB => 624MB
Number of used DB connections (Postgres):
Provisioning 100 instances: 5-11 average , 12 max => 1-2 average , 7-8 max.
Provisioning 100 instances and 100 jobs: 5-11 average, 16 max => 1-2 average, 35 max (short peaks when processing many submitted jobs).
Benchmarking notes:
Most measurements are median of 3.
No time means either I didn't want to wait for the benchmarks to finish or there were problems with running the setup.
Postgres connections count "active" + "idle in transaction" and don't count "idle" since dstack server uses a client connection pool.
Observations:
Processing >150 resources on a single replica was impractical due to high processing latency. Now a single replica can handle hundreds and possibly thousands of active resources without performance degradation.
Provisioning many instances became almost as fast as provisioning one instance – the dstack overhead is minimal. With the sufficient number of instances, processing throughput is limited by the provider's rate limits, e.g. 5 RPS for AWS RunInstances.
Some one-resource processing may not improve since it does not depend on processing throughput but on other factors such the number of state transitions and min processing intervals. In cases where pipelines have bigger latency, it's possible to optimize by tweaking processing intervals and implementing "fast" paths to skip unnecessary waits.
Provisioning many jobs wasn't limited by pulling but by processing latency so there was no difference between running on instances with and without pulled image before.
Running even a small number of runs was slow when the server already had many active resources, e.g. instances, possible due to the session pool limit. Pipelines don't have this issue, e.g. having many idle instances does not effect run processing much.
Running one N-node task takes more time than running N one-node tasks. This is expected since multi-node tasks need extra synchronization between jobs.
Pipelines may slightly increase memory usage due to higher processing throughput.
As expected, most DB connections before pipelines are "idle in transaction" and <3 are "active". With pipelines, there can be both many "active" and "idle in transaction" but less in total on average. Pipelines can have higher spikes during active provisioning. This is not a problem since they are short, can be handled with a client-side pool, and can be further optimized. TooManyConnectionsError should be less likely.
Activity
github-actions commentedon Mar 12, 2026
This issue is stale because it has been open for 30 days with no activity.
r4victor commentedon Mar 31, 2026
Benchmarking old tasks vs pipelines (one server replica, SQLite):
Benchmarking notes:
Observations:
TooManyConnectionsErrorshould be less likely.Benchmarks against #3719.