Gulp 4 Task Composition: When to Use series() vs parallel()
A decision guide for composing Gulp 4 tasks: when series() is mandatory, when parallel() is safe, and how to validate that your parallel tasks are truly independent.
09 Jul 2025, 19:36 UTC

Every Gulp 4 build eventually hits the same question: should these tasks run one after another, or all at once? The short answer is that gulp.series() is the safe default and gulp.parallel() is an optimization you earn by proving your tasks are independent. Getting this wrong doesn't just slow builds — it produces corrupted output that fails intermittently and is miserable to debug.
The decision and its constraints
Gulp 4 removed the Gulp 3 dependency-array syntax (gulp.task('name', [deps], fn)) and replaced it with two explicit composition functions. That change forces you to state your intent, but it also means the burden of correctness moved to you. Before choosing, answer two questions about each pair of tasks:
- Does one task read or depend on output that another task produces (e.g.,
cleanbeforebuild, transpile before bundle)? - Do the tasks write to the same destination directory or file?
If either answer is yes, those tasks belong in series. gulp.parallel() runs tasks concurrently, so any hidden ordering assumption becomes a race condition — sometimes the build works, sometimes dist ends up half-written, and the failure depends on timing and machine load.
Trade-offs at a glance
| Criteria | gulp.series() | gulp.parallel() |
|---|---|---|
| Ordering guarantee | Strict, sequential | None — tasks run concurrently |
| Wall-clock time | Sum of all task durations | Roughly the slowest task's duration |
| Correctness burden | Low — side effects are predictable | High — you must prove independence |
| Failure behavior | Stops at the failing task; prior steps completed cleanly | One error can abort the run while siblings are mid-write, leaving partial output |
| Best fit | clean → build → bundle chains | Independent work: linting while copying assets |
Two caveats on the speed column. First, parallel gains are bounded by I/O and plugin behavior, not just CPU cores — measure your actual build before restructuring around concurrency. Second, parallel composition can mask weak error handling: a task that throws while a sibling is writing to disk can leave dist in a state that looks plausible but is broken.
The most common composition bug
Whichever combinator you pick, every task must signal completion by returning a stream, a promise, or an event emitter — or by calling the done callback. A task that does async work without signaling completion makes Gulp hang or report that the task never finished. This is the single most frequent cause of "my build randomly stalls" reports, and it has nothing to do with series versus parallel.
Concrete implementation
The supported pattern for a staged pipeline is nesting: series for the stages, parallel inside a stage for genuinely independent work. This gulpfile assumes Gulp 4.x (check with npx gulp --version — Gulp 3 examples with dependency arrays will fail here):
const { series, parallel, src, dest } = require('gulp');
function clean() {
// del or fs.rm — must return the promise so Gulp knows when it's done
return import('del').then(({ deleteSync }) => {
deleteSync(['dist']);
return Promise.resolve();
});
}
function scripts() {
return src('src/js/**/*.js')
.pipe(dest('dist/js')); // returning the stream signals completion
}
function styles() {
return src('src/css/**/*.css')
.pipe(dest('dist/css'));
}
function lint() {
// independent of scripts/styles output — safe to run concurrently
return src('src/js/**/*.js'); // pipe through your linter here
}
// clean MUST finish first; scripts/styles/lint are independent of each other
exports.build = series(clean, parallel(scripts, styles, lint));
Run it from the project root with npx gulp build. No elevated permissions are needed; the only write target is dist/. The risk to be aware of: if clean is ever moved into the parallel group, it can delete files the other tasks have already written.
Watch mode is a special case
For development, keep destructive steps like clean in a series that runs once at startup, then compose a fast parallel set (lint, compile, reload) for incremental rebuilds. Running clean on every file change would erase output that other watch tasks are mid-way through updating.
How to validate the choice
Parallel composition bugs are nondeterministic, so a single successful run proves nothing. Three practical checks:
- Confirm ordering visually. Add a
console.logwith a timestamp at the top of each task and runnpx gulp build. Series tasks should always appear in order; parallel task logs will interleave. - Loop the build. From a shell in the project root:
for i in $(seq 20); do rm -rf dist && npx gulp build || break; done. Twenty clean runs that all produce identical artifacts is reasonable evidence of independence; intermittent failures almost always reveal hidden coupling between tasks. - Test the failure path. Temporarily make one parallel task throw, run the build, and inspect
dist. Check whether the run aborts cleanly and whether partial files were left behind — if they were, your CI needs to treat any task failure as a full rebuild trigger, not a retry.
The rule of thumb that survives all of this: start with everything in series, then move tasks into parallel groups only after the loop test passes. A build that is two seconds slower but deterministic beats a fast build you can't trust.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.