1.1.3 Server/Client Architecture
Every command from 1.1.1 Commands, whether build, test, or run, starts the same way: the bazel process you launch is only a client. It connects to a long-lived Bazel server associated with the workspace's output_base, and that server keeps an analysis cache of loaded BUILD files, the dependency graph, and related metadata in memory across invocations1. That is why the first command after startup feels cold while the second can feel instantaneous: the expensive loading and analysis work is often already sitting in the server's memory2.
One command, two processes
The client is short-lived: it parses flags, finds the right server, sends the request, prints output, and exits1. The server is the long-lived JVM process that does the heavy lifting and stays around in the background for later commands3. Bazel identifies that server by output_base, not by the shell process that launched it4.
That detail explains two everyday CLI behaviors. First, bazel build, bazel query, and bazel test can all benefit from the same warm state when they hit the same server1. Second, bazel info output_base (1.1.7 bazel info) tells you which on-disk tree anchors that server's state4.
Why the first build is slow
A cold invocation has to start the server, load and evaluate the relevant BUILD files, and construct the dependency graph in memory2. 2.2.1 Loading, Analysis & Execution later separates that work into loading, analysis, and execution. For now, the first command pays the setup cost, then repeated commands amortize it.
This shows up directly in build output. On the first build, Bazel reports packages loaded and targets configured. Repeating the same command immediately afterwards turns into a null build with both counts at zero5:
INFO: Analyzed target //foo:foo (14 packages loaded, 48 targets configured).
...
INFO: Analyzed target //foo:foo (0 packages loaded, 0 targets configured).
That speedup comes mostly from the server's in-memory analysis cache2. If the server restarts, or if Bazel discards the analysis cache because your options changed, the next build becomes cold again2.
Warm server vs persistent outputs
It helps to separate two different things that both make later builds faster.
The warm server keeps the analysis cache alive. 1.1.4 Inspecting Server State focuses on how to inspect that state from the CLI: what lives under output_base, what only lives in memory, and which commands reveal each layer. 2.4.1 Skyframe & Incrementality then explains the mechanism behind that cache. Lose that server, and Bazel has to reconstruct the dependency graph2.
The output tree under the output base (see 0.1.4 Output Root) is different. Those files live on disk, so bazel shutdown clears the analysis cache but does not automatically invalidate already-produced outputs6. A restarted server may still avoid rerunning actions if the outputs in bazel-out/ are still valid6. A "cold build" can therefore redo loading and analysis while reusing valid execution results instead of recompiling everything.
One at a time means one server
The usual rule of thumb is "Bazel only runs one command at a time per workspace," but the precise rule is narrower: one active invocation per server/output base1. If two commands use the default output base, they contend for the same server lock and one waits, or fails immediately if --noblock_for_lock is set1,7.
If you intentionally give commands different output bases, Bazel starts different server instances and they can run concurrently4:
OUTPUT_BASE=/var/tmp/google/_bazel_johndoe/custom_output_base
bazel --output_base ${OUTPUT_BASE}1 build //foo &
bazel --output_base ${OUTPUT_BASE}2 build //bar
Most day-to-day work should use the default behavior. Separate output bases are mainly for scripts, experiments, or workflows that need isolation4,7.
Server lifecycle
A Bazel server shuts itself down after being idle for 3 hours by default, though --max_idle_secs can change that1,4. You can also stop it explicitly with bazel shutdown, which is useful when scripted builds would otherwise leave idle daemons behind1,7.
Restarts happen for two common reasons. The obvious one is version mismatch: the client sees that the running server does not match the Bazel version it needs and starts a new one1. The subtler one is a startup-option mismatch. Options such as --output_base or --host_jvm_args apply to the server JVM itself, so if they differ from the running server's startup settings, Bazel restarts the server to get a matching process4.
That also explains why --batch is slower. Batch mode skips the normal client/server model, runs a one-shot Java process, and gives up the analysis cache that makes iterative CLI work fast4. Likewise, bazel clean --expunge (1.1.6 bazel clean) is deliberately destructive: it removes the whole output base and stops the server3.
When a repeated Bazel command is suddenly slow, think about server state first. Did the command hit the same output_base and reuse the same long-lived server, or did the server restart, time out, or discard its analysis cache? That mental model explains most "first build slow, next build fast" behavior before you ever need to learn Skyframe internals.
Check your understanding · 3 questions
1.Why is the first Bazel command after starting the server slower than subsequent ones?
Select one answer
2.True or false: what does bazel shutdown affect?
Choose True or False for each sentence
3.Which of the following cause the Bazel server to restart?
Select all that apply
Footnotes
-
Client/server implementation — Long-lived server, shared cache across commands, one invocation per server, idle timeout, version-based restart ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Optimize Iteration Speed — Analysis cache lives in the server, cold runs after restart or cache discard, separate output bases create separate servers ↩1 ↩2 ↩3 ↩4 ↩5
-
Commands and Options — Bazel server runs in its own JVM,
shutdownstops it, andclean --expungeremoves the output base ↩1 ↩2 -
Commands and Options —
--output_base, startup-option restarts,--batch,--max_idle_secs, and concurrent servers via different output bases ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Build programs with Bazel — Null build example showing repeated command with
0 packages loaded, 0 targets configured↩ -
The Many Caches of Bazel — Distinction between in-memory Skyframe state and persistent output tree cache that survives
bazel shutdown↩1 ↩2 -
Calling Bazel from scripts — Output-base lock, scripted server cleanup, and
--noblock_for_lockbehavior ↩1 ↩2 ↩3