HTCondor: Notes on an Open-Source Job Scheduler
Published on July 27, 2026
Working notes on HTCondor — a workload manager for high-throughput computing. This one showed up on TrexQuant’s job requirements, which is what sent me looking. Filling this in as I learn it.
Contents
- What is HTCondor?
- Why quant firms use it
- Architecture
- ClassAds and matchmaking
- Submitting a job
- Everyday commands
- Universes
- DAGMan — workflows with dependencies
- Things I still need to work out
- References
What is HTCondor?
HTCondor is an open-source workload manager: you hand it a description of a job, it finds a machine in a pool that can run it, ships the job there, runs it, and gets the output back to you. It comes out of the Center for High Throughput Computing at UW–Madison, where it started life as “Condor” in the 1980s before being renamed in 2012. It’s Apache 2.0 licensed.
The phrase to anchor on is high-throughput computing (HTC), not high-performance computing (HPC). The distinction is the thing HTCondor is actually organised around:
| HPC | HTC | |
|---|---|---|
| Optimises for | FLOPS over seconds/hours | Jobs completed over weeks/months |
| Typical job | One tightly-coupled MPI job across many nodes | Many independent jobs |
| Failure of one node | Kills the job | Reschedules one job |
| Hardware | Uniform, dedicated cluster | Heterogeneous, possibly borrowed |
If your workload is “run this simulation across 512 tightly-synchronised cores,” that’s Slurm/MPI territory. If it’s “run these 200,000 independent backtests and tell me when they’re all done,” that’s HTCondor.
TODO: expand — where the HTC framing actually changes design decisions.
Why quant firms use it
TODO: fill in. The shape of the argument I want to make:
- Parameter sweeps and backtests are embarrassingly parallel — the canonical HTC workload.
- Research compute is bursty; a scheduler that reclaims idle desktops/cycles is worth a lot.
- Fair-share and priority policy matters when many researchers contend for one pool.
- It’s free and self-hosted, so no per-core licensing on a large internal grid.
Verify how much of this is actually why, vs. my speculation.
Architecture
A pool has three roles. One machine can play more than one.
┌──────────────────────────┐
│ Central Manager │
│ collector + negotiator │
└───────────┬──────────────┘
│ matchmaking
┌───────────────┴────────────────┐
│ │
┌─────────▼─────────┐ ┌───────────▼─────────┐
│ Submit Node │ │ Execute Node │
│ schedd │──jobs──► │ startd → starter │
└───────────────────┘ └─────────────────────┘
| Daemon | Runs on | Job |
|---|---|---|
condor_master |
Every node | Supervises the other daemons, restarts them |
condor_collector |
Central manager | The pool’s directory — everything advertises itself here |
condor_negotiator |
Central manager | Matchmaking: pairs waiting jobs with available machines |
condor_schedd |
Submit node | Owns the job queue, manages submitted jobs |
condor_startd |
Execute node | Advertises the machine’s resources, accepts jobs |
condor_starter |
Execute node | Spawned per job; actually runs it and reports back |
TODO: trace one job end-to-end through these daemons.
ClassAds and matchmaking
The idea underneath everything else. Both jobs and machines describe themselves with ClassAds (classified advertisements) — sets of attribute/value pairs. Crucially, both sides state requirements and preferences:
- A job says: I need 4 GB of RAM and Linux, and I’d prefer a faster machine.
- A machine says: I’ll accept jobs from these users, and I prefer short ones.
The negotiator then finds pairs where each side’s Requirements expression evaluates true against the other’s ad, and ranks the candidates by each side’s Rank expression. It’s bilateral — the machine gets a say, which is what makes “borrow idle desktops but give them back when someone sits down” expressible as policy rather than special-cased code.
TODO: worked example of a job ad and a machine ad side by side, with the Requirements/Rank expressions that match them.
Submitting a job
A submit file describes the job; condor_submit hands it to the schedd.
executable = run_backtest.sh
arguments = --strategy $(strategy) --year $(year)
output = logs/out.$(Cluster).$(Process)
error = logs/err.$(Cluster).$(Process)
log = logs/job.log
request_cpus = 1
request_memory = 4GB
request_disk = 1GB
queue
- Cluster — the id of one
condor_submitinvocation. - Process — the index within that cluster. A job is identified as
Cluster.Process, e.g.1234.56. queue Nsubmits N copies; the richerqueue ... from ...forms are how you fan out over a parameter list.
TODO: the
queue from/queue matchingvariants, and the file transfer mechanism (should_transfer_files,transfer_input_files) — that’s the part that actually bites when the execute node has no shared filesystem.
Everyday commands
| Command | Does |
|---|---|
condor_submit job.sub |
Submit a job |
condor_q |
Show your queued jobs |
condor_q -better-analyze <id> |
Explain why a job isn’t running |
condor_status |
Show machines in the pool and their state |
condor_rm <id> |
Remove a job |
condor_hold / condor_release |
Pause / resume a job |
condor_history |
Look at completed jobs |
condor_submit_dag <file> |
Submit a DAGMan workflow |
condor_q -better-analyze is the one to remember — “my job is idle and I don’t know why” is the default HTCondor experience, and this is the tool that tells you which requirement failed to match.
TODO: annotate real output from each of these once I have a pool running.
Universes
The universe picks the runtime environment for a job.
| Universe | Use |
|---|---|
vanilla |
The default — any ordinary executable |
container / docker |
Run inside a container image |
parallel |
Multi-machine jobs (MPI) |
scheduler |
Runs on the submit node, doesn’t wait for a match (DAGMan uses this) |
local |
Runs on the submit node immediately |
grid |
Delegate to an external resource manager |
TODO: check which universes are current vs. deprecated in recent releases —
standardis gone, and I think the docker universe is now folded intocontainer. Verify before publishing.
DAGMan — workflows with dependencies
Most real pipelines aren’t a flat pile of independent jobs; step B needs step A’s output. DAGMan (Directed Acyclic Graph Manager) is HTCondor’s answer: you declare jobs and edges, it handles submission order, retries, and recovery from a partially-completed run.
JOB fetch fetch.sub
JOB train train.sub
JOB report report.sub
PARENT fetch CHILD train
PARENT train CHILD report
RETRY train 3
TODO: rescue DAGs — the recovery-after-failure story is the actual selling point and I don’t understand it yet.
Things I still need to work out
- Set up a single-machine pool locally and run a real job through it.
- File transfer without a shared filesystem — the mechanics and the failure modes.
- Priorities and fair-share: how
condor_userprioactually allocates between users. - Job checkpointing and preemption — what happens to a job when a machine is reclaimed.
- Flocking (spilling jobs into another pool) and glideins (pilot jobs onto external resources).
- Security: how authentication works in a pool that isn’t all one trusted host.
- How it compares to Slurm, Nomad, and Kubernetes Jobs, and when you’d pick which.
References
TODO: add links as I go.
- HTCondor manual — https://htcondor.readthedocs.io
- Center for High Throughput Computing, UW–Madison — https://chtc.cs.wisc.edu
Tags: htcondor, job_scheduling, distributed_systems, hpc