When working on a development workflow, itβs common to have several long-running processes: a frontend build watcher, a template compiler, and an application server. Instead of running these individually, Make can orchestrate them and run them in parallel.
Using make -j for concurrency
The -j flag tells Make how many jobs it may run at once. Any targets listed after the job count are executed in parallel, as long as they have no dependency relationship.
A simple example:
.PHONY: dev
dev:
@make -j3 tailwind templ server
In this setup, Make starts tailwind, templ, and server simultaneously, up to a maximum of three concurrent jobs.
Why use Make for this?
Unified workflow
Rather than relying on a custom shell script or manually starting each process, Make serves as a lightweight task runner. Developers only need a single entry point:
make dev
Built-in process supervision
If one process exits early, Make stops the others. This avoids orphaned watchers cluttering your terminal sessions.
Familiar portability
Because Make is already installed on most Unix-based systems, it keeps dependencies minimal.
Tips for a robust setup
Make every sub-task its own target
For example:
.PHONY: tailwind
tailwind:
npx tailwindcss -w
.PHONY: templ
templ:
templ generate --watch
.PHONY: server
server:
go run ./cmd/server
This keeps the dev target clean and each task reusable.
Use --output-sync to improve logs
Parallel processes can mix their output, making logs harder to read. GNU Make provides output synchronization:
make -j3 --output-sync=target dev
Each targetβs output stays grouped, even if they run concurrently.
This only does the grouping once you stop make, not while it's running.
Avoid implicit recursion
When using recursive make, ensure that .PHONY is set and the commands use @make (not ${MAKE} in very old setups) to avoid incorrect jobserver warnings.
When not to use make -j
If your tasks depend on each other (for example, a compiler producing output required by the server), running them in parallel may break your workflow. In that case, make them dependencies of one another instead of siblings.
Conclusion
Running multiple long-running tasks with make -j provides a compact and reliable development workflow. It keeps your tooling simple, improves portability, and makes your dev environment easier to maintain.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.