365 words, 2 min read

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.