What are Nx continuous tasks and what problem do they solve?

A continuous task is an Nx target marked as long-running, typically with continuous: true in its target configuration. Nx starts the task and keeps it alive instead of waiting for it to exit. Other tasks that depend on it can then start while it is still running. A dev server, an API in watch mode, or a local queue consumer fits this model because none of them is meant to finish.

Before this existed, dependsOn could only express "run X, wait for it to complete, then run Y." That works for builds and code generation, which exit. It fails for a backend API, which never exits. If serve for a frontend depended on serve for the API, Nx would wait forever for the API task to complete, and the frontend would never start.

Teams worked around this in a few ways:

  • Opening one terminal per service and starting them in the right order by hand.
  • Writing wrapper targets with nx:run-commands in parallel mode, sometimes using readyWhen to match a log line.
  • Adding a root-level script that called a process-runner package to start everything at once.

Each of these hid the real dependency graph from Nx, so the relationships between services lived in scripts and in people's heads.

Continuous tasks are a relatively recent addition. Before relying on them, check the field name and behavior against the docs for your installed Nx version.

They help most if you are a backend or platform engineer maintaining a workspace where one nx serve needs several services running before it is usable.

Who should use Nx continuous tasks?

Continuous tasks are worth adopting if your Nx workspace has more than one long-running process that must be up at the same time for local development or testing. Typical fits:

  • Frontend engineers who need a real backend. If the web app is unusable without the API, a mock server, or an auth stub, continuous tasks let nx serve web start those for you instead of relying on a README checklist.
  • Backend and platform engineers who own the local dev setup. If you maintain wrapper scripts, a process-runner config, or onboarding docs that list a start order, continuous tasks move that knowledge into project configuration, where Nx can read and enforce it.
  • Teams running end-to-end tests against a dev server. An e2e target that depends on a continuous serve target can bring the server up as part of the test run. Confirm in your Nx version's docs how the server is stopped when the test target finishes.
  • Monorepos with shared, generated code. If the API depends on codegen or library builds, continuous tasks combine those finite, cacheable steps with the long-running processes in one graph.

They add little for a workspace with a single app and no local backend, or where the "services" are mostly stateful infrastructure such as databases and message brokers. That infrastructure is often better managed outside Nx, as covered later in this article.

How do Nx continuous tasks work?

Nx builds a task graph from the project graph and each target's dependsOn configuration, then executes it. For an ordinary task, a dependent task starts only after the dependency exits successfully. Marking a target as continuous changes one rule: Nx considers the dependency satisfied once the process has been started, not once it has exited. Dependents can start while it keeps running.

In practice, a run looks like this:

  1. You run a target, for example nx serve web.
  2. Nx resolves every upstream task through dependsOn, including continuous ones such as api:serve.
  3. Finite tasks (builds, codegen) run to completion first and may be restored from cache.
  4. Continuous tasks are spawned in dependency order and left running.
  5. Nx keeps all of them under one command. When you stop that command, Nx tears down the task tree it started.

Nx tracks only the processes it launched. It does not know about a service you started in another terminal, and it does not check whether a spawned process is ready to accept connections. The next sections cover both points in more detail.

How does Nx resolve a chain of continuous and non-continuous tasks?

dependsOn composes the same way for long-running targets as for any other target. Nx builds one task graph from the project graph and your target configuration, then walks it. Two kinds of edges matter here.

Continuous tasks depending on finite tasks

A serve target can depend on targets that run and exit, such as building a shared library, running code generation, or compiling protobuf definitions. Nx treats these as ordinary tasks. Each one has to finish successfully before anything that depends on it starts. Because they are finite and deterministic, they are cacheable. If nothing in their inputs has changed, Nx restores their outputs instead of rerunning them. This is why a codegen step that takes a while on a cold start becomes close to free on the next nx serve.

Continuous tasks depending on continuous tasks

A continuous target can also depend on another continuous target. For example, web:serve depends on api:serve, which depends on a database container or a mock service target. Nx starts the dependency, does not wait for it to exit (it never will), and then starts the dependent process. The whole chain is resolved from the graph, so nx serve web brings up everything below it.

What Nx does not do

Nx orders process startup, not readiness. When the web server launches, the API process has been spawned, but it may not be listening on its port yet. The section on readiness, ports and environment configuration below covers how to deal with that gap.

Caching applies only to the finite upstream steps. Continuous tasks are never cached. The time you save on the continuous side comes from orchestration: one command instead of several terminals and a remembered start order.

                  $ nx serve web
                        |
  CONTINUOUS            |               FINITE
  (never cached)        v               (must exit 0, cacheable)
  +--------------------+  dependsOn   +---------------------------+
  | web:serve          |------------->| shared-ui:build           |
  | spawned, no exit   |              | inputs same -> restore    |
  +--------------------+              +---------------------------+
            |
            | dependsOn
            v
  +--------------------+  dependsOn   +---------------------------+
  | api:serve          |------------->| api-client:codegen        |
  | spawned, no exit   |              | inputs same -> restore    |
  +--------------------+              +---------------------------+
            |
            | dependsOn
            v
  +--------------------+
  | db:serve / mock    |
  | spawned, no exit   |
  +--------------------+

  Startup order Nx enforces (bottom-up):
  db spawned ----------+
  codegen finished ----+--> api spawned --+
  shared-ui finished ---------------------+--> web spawned

  Spawned is not ready: api may not be listening when web starts.

Nx resolves one task graph in which continuous serve targets depend on other continuous targets and on finite, cacheable build and codegen tasks, enforcing process start order but not readiness.

How do you configure nx serve to start a backend API automatically?

Take a workspace with three projects: web (a frontend), api (a Node backend), and api-client, a library whose codegen target generates TypeScript types from the API's OpenAPI schema. You want nx serve web to bring up everything the frontend needs.

Step 1: mark the API's serve target as continuous. In the api project configuration, set "continuous": true on the serve target. This tells Nx the process does not exit on success. Without the flag, Nx treats api:serve as a finite task and waits for it to finish before starting anything that depends on it. A dev server never finishes, so web would never start.

Step 2: make the web app depend on it. On web's serve target, add "dependsOn": ["api:serve"]. Nx sees that the dependency is continuous, starts it, and then starts web:serve alongside it instead of after it.

Step 3: put the finite work upstream. On api:serve, add a dependsOn entry pointing at api-client:codegen, or at ^build if the API imports buildable libraries. These targets are not continuous, so Nx runs them to completion first, using the cache where it can, before it launches the API. Keep the ordering in this direction. If a finite task depended on a continuous one, it would wait on a process that never exits.

Run nx serve web. Nx runs codegen, starts the API, and starts the frontend from a single command, with no shell script or process manager in front of it.

Configuration keys and dependsOn syntax have changed between Nx releases. Check them against the documentation for the Nx version your workspace uses.

{
  "targets": {
    "serve": {
      "executor": "nx:run-commands",
      "options": { "command": "node apps/api/src/main.js" },
      // Long-running process: Nx should not wait for it to exit
      "continuous": true,
      // Finite work runs to completion (or is restored from cache) first
      "dependsOn": ["api-client:codegen"]
    }
  }
}

{
  "targets": {
    "serve": {
      "executor": "nx:run-commands",
      "options": { "command": "vite --port 4200" },
      "continuous": true,
      // api:serve is continuous, so web starts alongside it, not after it
      "dependsOn": ["api:serve"]
    }
  }
}

$ nx serve web

Two project.json excerpts show a continuous api:serve that depends on a finite codegen step, and a web:serve that depends on api:serve, so that nx serve web runs all three from one command.

How do you handle readiness, ports and environment configuration for local dev?

A continuous task counts as "started" once its process launches, not once it accepts connections. The backend may still be compiling, running migrations or binding its port when the frontend dev server sends its first proxied request. The result is a connection-refused error, or a 502 from the dev proxy that looks like an API bug. Check your Nx version's documentation and observe the behaviour directly: start the chain and watch whether the frontend waits for the API to listen or starts right away.

Approach Where it lives Strength Weakness
Retry or backoff in the dev proxy Frontend tooling No extra task, and it recovers from backend restarts Hides real outages if retries run too long
Health check in backend startup Backend code Readiness means "dependencies reachable", not only "port open" Doesn't stop the frontend from starting early
Wait-for-port step Small task before serve Simple and explicit gate Checks only the socket, not application health

Ports and URLs

Assign each project a fixed port in its own configuration so the port doesn't depend on startup order. Don't hardcode the backend URL in frontend source. Read it from a project-level env file or the dev proxy target, so changing it means editing one value.

When two frontends share one API, point both at the same URL. Don't give each one its own copy of the API: two copies would collide on the fixed port, or, with ports shifted, silently split state between instances. If a port is already in use, find the process holding it before you change any numbers. Usually it's a stale copy left by an earlier session.

#!/usr/bin/env bash
set -euo pipefail

HOST="${API_HOST:-127.0.0.1}"
PORT="${API_PORT:-3333}"
TIMEOUT_SECONDS="${WAIT_TIMEOUT:-60}"

for ((i = 0; i < TIMEOUT_SECONDS; i++)); do
  # Bash's /dev/tcp opens a TCP connection. The subshell closes it right away.
  if (exec 3<>"/dev/tcp/${HOST}/${PORT}") 2>/dev/null; then
    echo "API is listening on ${HOST}:${PORT}"
    exit 0
  fi
  sleep 1
done

echo "Timed out after ${TIMEOUT_SECONDS}s waiting for ${HOST}:${PORT}" >&2
echo "Check whether a stale process holds the port before changing numbers:" >&2
echo "  lsof -nP -iTCP:${PORT} -sTCP:LISTEN" >&2
exit 1

A minimal wait-for-port gate that reads the API port from shared env values, fails loudly after a timeout and suggests how to find a stale process holding the port.

What happens to logs, crashes and Ctrl+C with multiple long-running tasks?

That stale copy is usually a side effect of running several long-lived processes from one command, so it helps to know how Nx behaves when things go wrong.

Reading interleaved output

When nx serve starts the frontend and one or more backends, each process writes to its own stdout and stderr. Depending on your Nx version and output settings, you will see either a terminal UI with a pane per task or a single stream with lines from different tasks mixed together. In a mixed stream, a stack trace from the API can be split by a rebuild message from the frontend, and a log line without its task prefix is easy to misattribute. Separate terminals make it obvious which process printed what.

When one process crashes

If the API exits, the dev server usually keeps running and keeps serving the UI. The page loads, but every request fails with a proxy error or a connection refused. Before you debug the frontend, check whether each continuous task is still alive. Look for an exit message in that task's output, or check whether anything is still listening on its port.

After Ctrl+C

Confirm that the processes have actually stopped. A child that ignores the signal, or that a wrapper script spawned, can outlive its parent. Run lsof -i :<port> or ss -ltnp to see whether anything still holds the API's port. Run ps with a filter on the process name to find orphaned node or language-runtime processes, then stop them with kill.

When you need to debug a backend in detail, run its serve target on its own in a dedicated terminal. You get clean logs, and you can attach a debugger to a single process.

lsof -i :3333 -i :4200
ss -ltnp | grep -E ':(3333|4200)\b'

ps -eo pid,ppid,etime,args | grep '[n]ode'
kill 12345            # sends SIGTERM
sleep 2
kill -0 12345 2>/dev/null && kill -9 12345

lsof -i :3333 || echo 'port 3333 is free'

npx nx serve api

Shell commands to check whether ports are still held and child processes survived Ctrl+C after nx serve, stop any orphans, and then run a single backend on its own for debugging.

When should you avoid continuous tasks or keep infrastructure outside Nx?

When the convenience costs too much

A single nx serve that silently starts an API, a worker, a mock auth server, and a database adds CPU, memory, and startup time. Nobody chose that cost. It arrived through dependsOn. Someone editing CSS waits for services they never touch, and laptops with less RAM feel it first. If a dependency chain makes the first useful reload noticeably slower, that chain is too heavy for the default target.

Continuous tasks also cause trouble when the service is already running, for example in a container or in a terminal you opened to debug it. Nx does not know about that process. It starts a second copy, which either fails on a port conflict or quietly runs alongside the first. The same applies to teammates who start backends with their own scripts because not everything in the repo runs through Nx.

Opting out without fighting the graph

dependsOn belongs to the target, so the most reliable opt-out is a second target. Keep serve lean, with no continuous dependencies, and add a target such as serve-full or dev that declares the backend services. Developers then choose which graph to run. Nobody has to kill a duplicate process after it starts.

Infrastructure: wrap it or leave it out

Wrapping docker compose up or a database as a continuous target is reasonable only if it runs in the foreground, so stopping Nx stops the containers. In detached mode, the containers outlive the Nx process, and the next run either reuses stale state or collides with it. Wrapping also creates two sources of truth: ports and credentials live in the compose file and again in Nx target options or env files, and the two drift apart.

For stateful, slow-starting infrastructure, manage it separately. Start it once with its own tooling and let Nx tasks assume it is reachable. Use continuous tasks for application processes that start and stop with the dev session.

What are the alternatives to Nx continuous tasks, and how do you migrate?

Most teams arrive at continuous tasks from one of three existing setups.

Approach Setup effort Onboarding Readiness Logs Teardown Maintenance
Multiple terminals None Slow, depends on tribal knowledge Human watches output Clear but scattered Manual, easy to leave orphans Lives in a README that drifts
Wrapper script (shell or concurrently-style) Low One command Ad hoc sleeps or polling loops Interleaved, prefixing varies Signal handling often incomplete Grows with every new service
Standalone docker compose Medium One command, needs Docker Healthchecks, if defined Per-service prefixes Reliable for containers Separate dependency graph to keep in sync
Nx continuous tasks Low once targets exist One nx serve Your target must signal it Grouped by task in the terminal UI Nx stops the whole task tree Graph already exists in project config

Continuous tasks save the most time when wrapper scripts duplicate the project graph. A wrapper has to know that the web app needs the API, and that knowledge goes stale as projects move. With continuous tasks, dependsOn encodes that relationship once and every entry point reuses it. Compose keeps its advantage for containerized infrastructure. The two tools coexist well.

Conversion checklist

  1. Identify long-running targets. Look for serve, watch or start targets that never exit.
  2. Mark them continuous in the target configuration. Nx then stops waiting for them to complete.
  3. Wire dependsOn from the consuming target to each backend target, and remove the equivalent logic from wrapper scripts.
  4. Add a readiness strategy, such as a health-check wait or retrying client, rather than fixed sleeps.
  5. Pin ports in configuration so parallel services never race for the same one.
  6. Test teardown and crashes. Press Ctrl+C and confirm that no process is still holding a port. Then kill one dependency and check what the others do.

How can continuous tasks save engineering time?

The savings come from four places. None of them comes from making individual processes faster.

  • Fewer manual start-ups. One command replaces opening several terminals and starting services in the right order. This matters most after branch switches, dependency updates and machine restarts, when the whole stack has to come up again.
  • Cached upstream work. Codegen and library builds that feed a continuous task are ordinary cacheable tasks. When their inputs have not changed, Nx restores outputs instead of rerunning them, so restarting the stack does not repeat that work.
  • Less script maintenance. The dependency between the web app and the API lives in dependsOn, next to the projects. Adding a service means editing project configuration, not updating a wrapper script, a README and a process-runner config separately.
  • Faster onboarding and fewer "works on my machine" sessions. A new engineer runs nx serve web instead of following a start-order checklist. Teammates are less likely to debug a frontend error that is really an API that was never started.

To size the benefit for your team, a rough calculation helps. The following is an illustrative estimate, not a measured result. All inputs are assumed:

  • Assumed team size: 8 engineers who run the full local stack.
  • Assumed full-stack restarts per engineer per working day: 3.
  • Assumed time saved per restart by replacing manual multi-terminal startup with one command: 1.5 minutes.
  • Assumed working days per month: 20.

8 × 3 × 1.5 × 20 = 720 minutes, or about 12 engineer-hours per month under these assumptions. Substitute your own numbers. If your team already uses a single wrapper script that works reliably, the per-restart saving is close to zero, and the main benefit is lower maintenance as the graph changes. The estimate also ignores costs: the setup work, the extra resources a heavy default serve chain consumes, and the readiness and teardown issues described above. Weigh those against the savings before you make every serve target pull in the full stack.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to Nx continuous tasks, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Changing a shared variable is one edit and one review instead of editing every service's .env by hand.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.