Crux Daemon · 4. Startup and lifecycle

If the daemon will not start, §4.3 has your answer: all 25 conditions that abort boot, each with its message, its line of source, and the exact fix. The rest of this chapter is the ordered boot sequence, the background tasks it spawns, and how it shuts down.

This chapter is reference. Read §4.3 first when something is broken; read §4.2 when you need to know what happens before what.

4.0 In plain English

Starting the daemon is not one action. It is a fixed, ordered sequence of 67 steps: read the environment, check the auth posture, open the data directory, take the locks, replay what is on disk back into memory, bind the listeners, then spawn the background tasks that keep running for as long as the process lives. The nearest familiar thing is an aircraft pre-flight checklist. The order is fixed, each item is checked, and if a check fails the aircraft does not take off.

That refusal is the point of the design, not a rough edge. Twenty-five distinct conditions abort boot rather than letting the daemon come up half-configured, and §4.3 lists every one with its exact message, the line of source that emits it, and the fix. A daemon that refuses to start tells you what is wrong in one line. A daemon that starts anyway and is quietly missing its auth mode tells you nothing until something much worse happens.

The moment you will need this chapter is the unhappy one: it will not start, you have a message and no context, and you want the answer rather than a tour. Go straight to §4.3 and match the message. The second moment is quieter but just as real: something is behaving oddly and you need to know whether a given piece of state is rebuilt at boot or carried over, which is §4.7, or whether some background task is meant to be running at all, which is §4.6.

The thing people get wrong is assuming that because the daemon started, it is fully configured. It is not the same claim. §4.4 lists the silent degradations: the conditions that leave a feature switched off, an optional dependency unreachable, or a subsystem inert, while boot completes and the process reports itself healthy. If you are debugging "why is this feature doing nothing", read §4.4 before you read anything else, because a green start is not evidence that the thing you configured is running.

4.1 Before main: arguments, and the flag that is silently ignored

#[tokio::main] async fn main() begins at main.rs:270. Its first action is CLI dispatch, deliberately short-circuited before load_config() "so they never start the daemon, read env, or touch the filesystem" (main.rs:272).

parse_cli_arg (main.rs:231) is a hand-rolled matcher on only the first argument. Clap was deliberately not pulled in, "keeping the env-only design intact".

ArgumentAction
--version, -V, versionPrint version_line() to stdout, exit 0 (main.rs:276)
--help, -h, helpPrint help_text() to stdout, exit 0 (main.rs:284)
mcp-stdioRun the stdio-to-HTTP MCP bridge and exit, not the daemon (main.rs:291)
self (e.g. self update, self update --check)Run the self-updater and exit (main.rs:294)
anything else, or no argumentsStart the daemon (main.rs:297)

corecruxd accepts no runtime configuration flags, and silently ignores unrecognised ones. help_text() states it verbatim: "It takes no runtime configuration flags, all configuration is supplied via environment variables." A flag such as corecruxd --data-dir /x starts the daemon normally and ignores the flag. There is no unknown-argument error. If you thought you set something on the command line, you did not.

version_line() is corecruxd <CARGO_PKG_VERSION> (<CORECRUX_GIT_SHA|unknown>) (main.rs:242). A Docker build without --build-arg GIT_SHA=... self-reports (unknown), because the build context excludes .git so the git fallback cannot fire.

The mcp-stdio bridge reads CRUX_MCP_URL (default http://127.0.0.1:14801/mcp) and an optional CRUX_AGENT_TOKEN (main.rs:260).

4.2 The 67-step boot sequence, in order

Every step is in crates/corecruxd/src/main.rs. "Fatal" means the daemon exits non-zero before serving.

#StepLocationFatal?
1load_config(): parse the YAML file if any, then env vars, into Configmain.rs:300; impl config.rs:793no
2config.validate_embedding_selection()main.rs:301; impl config.rs:609yes
3Assert auth_mode_explicitly_setmain.rs:304yes
4Assert the auth mode parsed, fail closed on a typomain.rs:313yes
5Authz::from_env(config.auth_mode), load mode-specific secrets (JWT secret or JWKS)main.rs:320yes
6Resolve the MCP agent-token registrymain.rs:326; impl main.rs:1983yes unless overridden
7validate_network_auth_posture(...)main.rs:333; impl main.rs:2022yes
8validate_mcp_bind_posture(...)main.rs:342; impl main.rs:2050yes
9A 70-field tuple binding that keeps config fields live on CPU-only buildsmain.rs:349no
10init_tracing(&config.log_level), the first point at which anything is loggedmain.rs:421; impl main.rs:2065no
11Enterprise trust-root validation, only if enterprise_trust_root is setmain.rs:422yes
12Content-manifest load and optional signature verify, only if content_manifest_path is setmain.rs:446yes
13Install the panic hook, logs panic.payload and panic.location via tracing::error!main.rs:457no
14create_dir_all(state_dir), then create_dir_all(data_dir)main.rs:475yes
15acquire_lock(&data_dir), exclusive flock on <data_dir>/LOCKmain.rs:477; impl main.rs:2156yes
16ControlHandle::load_or_init(<data_dir>/CONTROL.json)main.rs:479yes
17Build BuildInfo { version, commit }main.rs:483no
18Metrics::new(...); register redaction metrics; seed gaugesmain.rs:488no
19load_or_init_node_meta(<data_dir>/meta/node.json)node_idmain.rs:500yes
20LocalPassportKey::from_path(&config.passport_key_path)main.rs:511yes
21Mint the RCX free-local capability token (self-signed, 366-day validity) and build RcxRoutermain.rs:512no
22Optionally spawn the anonymous passport claim (network call) if passport_claim_on_startupmain.rs:535no (background)
23ShardMapStore::new(&data_dir).load_or_init(...)RoutingTable::new(...)main.rs:545yes
24Initialise Readiness::default()main.rs:562no
25let dataplane_pool: Option<DataPlanePool> = None;, hard-codedmain.rs:564-
26reconcile_control_checkpoint_with_evidence(...), seeds the control-evidence readiness fieldsmain.rs:567no
27Create the shutdown broadcast channel (capacity 1) before any task spawnsmain.rs:581no
28spawn_routing_reloader(...), background task 1main.rs:588no
29Measure data-dir space, build CapacityStatemain.rs:600no
30spawn_capacity_guard(...) if enabled, background task 2main.rs:620no
31update::initial_status(&config)main.rs:634no
32spawn_shutdown_signal(...), SIGINT and SIGTERM handlermain.rs:636no
33update::spawn_update_checker(...), background task 3main.rs:637no
34Open CreditMeterStore at <data_dir>/credit-meter.jsonl if enabledmain.rs:644yes if enabled and the open fails
35Open FactStore: persistent if fact_persistence_enabled, else in-memorymain.rs:652yes if the persistent open fails
36cost::init_persistence(&data_dir), replays journalled cost reports; no-op unless the cost lens is onmain.rs:658no
37Build ProjectionState; replay relations.jsonl into itmain.rs:663no (warn, start empty)
38RepoWatchService::maybe_new(...)main.rs:671no
39Warn if sync_mutual_auth is on but no sync_peer_trust_root is setmain.rs:677no
40Construct AppState: ~90 fields, including opening SessionStore, EntityStore, EdgeStore and WitnessProofStore, loading .ccxi retrieval indexes, and building SessionServicesmain.rs:683yes for the store opens
41Wire the shared EventBus into fact_store and session_store for SSEmain.rs:869no
42repo_registry::fail_incomplete_scans(...), mark scans in flight at last shutdown as failedmain.rs:872no
43repo_watch.start_existing_repos()main.rs:888no
44Dense-embedder selection: three-way precedence, see §4.5main.rs:892yes for a misconfigured delegation
45Semantic near-duplicate threshold wiringmain.rs:981no (warns if no embedder)
46Bootstrap seed: BootstrapSeeder.seed(), "always seed agent-facing documentation on startup (idempotent)"main.rs:992no
47Optional default-passport seeding (CORECRUXD_SEED_DEFAULT_PASSPORTS, default off) plus unconditional default-project seedingmain.rs:1004no
48Lens-kind registration: crux_lens_features::bootstrap_kinds and agentgraph_kinds::bootstrapmain.rs:1037no (warn and continue)
49spawn_ephemeral_gc(...): background task 4, gated by CORECRUXD_EPHEMERAL_GC, default offmain.rs:1054no
50spawn_consolidation_scheduler(...): background task 5, gated by CORECRUXD_CONSOLIDATION_SCHEDULER, default offmain.rs:1061no
51Near-duplicate router sweep (15s interval), background task 6, only if a dedup threshold is setmain.rs:1073no
52Build the shared McpContext if config.mcp_enabled, shared with the HTTP OpenAI shimmain.rs:1113no
53mcp_app = mcp_context.map(crux_mcp::server::router)main.rs:1176no
54Open CaseStore: passed to the router via an Extension layer, not via AppStatemain.rs:1184yes if the persistent open fails
55Build the HTTP router: http::router(...)apply_ingress_limits(...).layer(TraceLayer::new_for_http())main.rs:1195no
56Session TTL reaper (60s), background task 7main.rs:1199no
57Observation retention (hourly, 30s initial delay), background task 8, only if CORECRUXD_OBS_RETENTION_DAYS parses above 0main.rs:1220no
58Background sync loop: background task 9, only if sync is enabled with a non-empty remote URL; 5s initial delaymain.rs:1258no
59Background witness submission: background task 10, only if witness_enabled; 5s initial delaymain.rs:1325no
60Log the corecruxd starting banner: HTTP/gRPC/MCP addresses, data dir, commit level, append lane, tenant stamp modemain.rs:1424no
61Spawn the HTTP server task, serve_http(http_addr, app, rx, drain_cap)main.rs:1442-
62Emit the once-per-boot, consent-gated daemon_start usage ping on the blocking poolmain.rs:1448no
63Spawn the gRPC server task, builds DataPlaneService and ExportService, calls grpc::serve(...)main.rs:1459-
64Register periodic integration jobs on the SyncScheduler: github-sync (registered unconditionally, self-skipping) and the vault watcher (double-gated), background task 11main.rs:1486no
65Spawn the MCP server task if mcp_app is present, same ingress limits as the API planemain.rs:1583-
66tokio::try_join! on the HTTP, gRPC and MCP runners, main blocks here until shutdownmain.rs:1591-
67drop(lock_file), release the LOCK flock; return Ok(())main.rs:1601-

Two consequences of the ordering are worth internalising.

Nothing is logged before step 10. Steps 1 to 9 include the config parse and every auth-posture rail. If the daemon dies in that window you get a message on stderr from main returning an error, and no log line at all, including no indication of which config file was read, because config-file failures are silent (see chapter 5 §5.3).

The shutdown channel is created before any task spawns (main.rs:581). The comment explains why: the routing reloader and capacity guard "would otherwise outlive SIGTERM and hold the runtime open past graceful_shutdown_on_sigterm's 5s budget."

4.3 Everything that can refuse to start

The complete fail-closed list. Each aborts main with a non-zero exit and a message on stderr.

#ConditionMessage or behaviourFixLocation
1CORECRUXD_AUTH_MODE unset, and daemon.auth_mode absent from the YAML fileCORECRUXD_AUTH_MODE must be set explicitly; see config.example.envSet CORECRUXD_AUTH_MODE to one of off, dev_scopes, jwt_hs256, jwt_jwks, or set daemon.auth_mode in the config file. Either satisfies it.main.rs:304
2CORECRUXD_AUTH_MODE set to an unrecognised value`unknown CORECRUXD_AUTH_MODE <bad>; valid values: off, dev_scopes, jwt_hs256, jwt_jwks`Fix the spelling. Parsing is case-sensitive per arm: off/OFF work, Off does not. This is deliberate, "an unknown or typo'd auth mode must abort, never degrade to dev scopes".main.rs:313
3Authz::from_env fails, e.g. jwt_hs256 selected but CORECRUXD_JWT_HS256_SECRET missingPropagated as InvalidInputSet the secret for the mode you chose. HS256 needs CORECRUXD_JWT_HS256_SECRET; JWKS needs one of CORECRUXD_JWT_JWKS_JSON, _PATH, _URL or CORECRUXD_JWT_OIDC_DISCOVERY_URL.main.rs:320
4An MCP agent-token env var is present but fails the strength policy<err>. Fix the agent token to enable MCP auth, or set CRUX_MCP_ALLOW_EMPTY_AGENT_REGISTRY=1 to run with no MCP auth (local dev/tests only).Make each token 32 to 256 bytes from [A-Za-z0-9._~-]. For local dev only, set CRUX_MCP_ALLOW_EMPTY_AGENT_REGISTRY=1.main.rs:326, main.rs:1996
5Auth mode is off or dev_scopes and HTTP or gRPC binds a non-loopback addressauth mode {:?} may not bind to non-loopback addresses (http=…, grpc=…) without CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1Either switch to jwt_hs256/jwt_jwks, or keep the binds on loopback and publish the port through a proxy, or accept the risk with CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1.main.rs:2032
6A JWT auth mode and commit_level == ReplicatedCommit and CORECRUXD_REPLICATION_AUTH_BEARER unset or blankReplicatedCommit with JWT auth requires CORECRUXD_REPLICATION_AUTH_BEARER for follower replicationSet CORECRUXD_REPLICATION_AUTH_BEARER to a non-blank value, or leave CORECRUXD_COMMIT_LEVEL at its default local_commit. In this edition replicated commit cannot become ready anyway, see chapter 9 gate 3.main.rs:2040
7MCP enabled and binding non-loopback and the agent registry is empty and no overrideMCP may not bind to non-loopback address (<addr>) without CRUX_AGENT_TOKEN/CRUX_AGENT_TOKENS or CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1Set CRUX_AGENT_TOKEN or CRUX_AGENT_TOKENS, or bind MCP to loopback, or set CORECRUXD_MCP_ENABLED=0.main.rs:2056
8CORECRUXD_COMPUTE_PROVIDER and CORECRUXD_EMBED_DELEGATE_URL both set…are mutually exclusive to prevent delegation cyclesChoose one. A node either serves embedding work for peers or delegates it, not both.config.rs:616
9Embedding delegation partially configured, _TOKEN or _DIMENSIONS set without _URLCORECRUXD_EMBED_DELEGATE_URL is required when embedding delegation is configuredSet CORECRUXD_EMBED_DELEGATE_URL, or unset the other two.config.rs:621
10CORECRUXD_EMBED_DELEGATE_URL and CORECRUXD_EMBEDDING_URL both set…are mutually exclusiveChoose one embedding source.config.rs:624
11Delegation configured without CORECRUXD_EMBED_DELEGATE_TOKENCORECRUXD_EMBED_DELEGATE_TOKEN is required when embedding delegation is configuredSet the token. This is re-checked a second time at main.rs:898.config.rs:627
12Delegation configured with CORECRUXD_EMBED_DELEGATE_DIMENSIONS unset or 0…must be a positive integer…Set it to the delegate's real vector dimensionality. An unparseable value deliberately parses to 0 so it fails closed rather than reading as unset.config.rs:630
13Delegation configured with an empty CORECRUXD_EMBEDDING_MODELCORECRUXD_EMBEDDING_MODEL must be non-empty when embedding delegation is configuredSet the model name the delegate expects.config.rs:635
14DelegatingEmbedder::new rejects the configurationembedding delegation configuration is invalid: <err>Read the wrapped error; it names the field.main.rs:918
15Enterprise trust root present but invalidinvalid enterprise trust root: <comma-separated issue codes>Fix enterprise.customer_id, backend_id, trust_root_kid or trusted_issuer_kids per the issue codes, or set enterprise.enabled: false.main.rs:422
16The content manifest fails to load or verifyPropagated error from load_content_manifestFix or remove content.manifest_path; if the failure is a signature, either supply the right manifest or set content.verify_signatures: false.main.rs:446
17create_dir_all on state_dir or data_dir fails, permissions, read-only filesystemI/O errorCheck ownership. The container image runs as UID 65532; a bind-mounted host directory must be chowned by you.main.rs:475
18<data_dir>/LOCK is already flocked, another corecruxd is running on the same data dirtry_lock_exclusive errorStop the other daemon. Note the lock is per resolved path: two daemons started from different working directories against the default relative data_dir will not collide, and will quietly diverge.main.rs:477, main.rs:2156
19CONTROL.json exists but is not valid ControlV1 JSONserde_json errorRepair the JSON, or move it aside, the daemon writes a fresh one with defaults. Moving it aside clears any operator valves, including read_only and emergency_brake.main.rs:480, control.rs:138
20meta/node.json unreadable or unwritableI/O errorCheck permissions on <data_dir>/meta/. Deleting the file gives the daemon a new node_id, which stales shard-map entries.main.rs:501
21The passport key at passport_key_path is unreadable or malformedError from LocalPassportKey::from_pathRestore the key from backup. Do not delete it to "fix" the error: the passport key encrypts stored integration credentials via a derived subkey, so losing it loses those credentials, and every receipt signed by the old key stops verifying.main.rs:511
22The shard map fails to load or initialise, or RoutingTable::new rejects itErrorInspect <data_dir>/meta/routing/. current holds the active version; the shardmap.v*.json files are immutable and can be hand-restored.main.rs:548
23Credit meter enabled but credit-meter.jsonl cannot be openedI/O errorCheck permissions, or set CORECRUXD_CREDIT_METER=0.main.rs:644
24A fact, session, entity, edge or case store fails to open for persistenceI/O errorCheck permissions and free space on the data dir. As a diagnostic only, CORECRUXD_FACT_PERSISTENCE=0 runs the fact and session stores in memory, all writes are then lost on restart.main.rs:653, main.rs:804, main.rs:1187
25Any of the three listeners fails to bind, port in use, permission deniedTcpListener::bind error surfaced through the try_join!Free the port or change it. Remember all three planes share a fate: a gRPC bind failure on 4007 kills the HTTP plane too. On Linux, ports below 1024 need a capability the non-root container user does not have.main.rs:2217, main.rs:1591

4.4 The silent degradations: what does not stop it

These log a warning and continue. Each is a case where the daemon is running but is not doing what you configured, so they are worth an alert rule.

SituationWhat actually happensLocation
Durable session wiring failsFalls back to ephemeral in-memory sessionsmain.rs:836
relations.jsonl replay failsStarts with an empty ProjectionStatemain.rs:666
WitnessProofStore replay failsStarts emptymain.rs:702
Console or onboarding settings unreadableDefaultsmain.rs:848
WASM engine init fails (feature builds only)kind: wasm extension requests return 503; kind: external_tool extensions keep workingmain.rs:158
Lens-kind bootstrap errorsWarn and continuemain.rs:1042
CORECRUXD_SYNC_PEER_SIGNING_KEY is not valid 32-byte hex, or CORECRUXD_SYNC_PEER_TOKEN is not valid capability-token JSONSync peer auth is silently disabled and falls back to bearermain.rs:174
sync_mutual_auth on without a valid CORECRUXD_SYNC_PEER_TRUST_ROOTWarn: "tenant sync requests will fail closed"main.rs:677
CORECRUXD_DENSE_MODEL=fastembed on a binary built without the dense-embed-model featureWarns and uses LocalHashEmbeddermain.rs:971
CORECRUXD_SEMANTIC_DEDUP set with no dense embedderWarn; dedup inactivemain.rs:983
A malformed or unreadable YAML config fileNo log line at all. Every value silently falls back to its default. See chapter 5 §5.3config.rs:709
An unparseable CORECRUXD_*_PORT or _HOSTSilently binds the defaultconfig.rs:800

4.5 Dense-embedder selection

Three-way, first match wins (main.rs:892). The in-code comment states the intent: "Startup validation rejects an ambiguous or incomplete delegation configuration, so this branch never silently falls through to a different semantic space."

  1. CORECRUXD_EMBED_DELEGATE_URL setDelegatingEmbedder, authenticated daemon-to-daemon delegation. Requires _TOKEN and _DIMENSIONS; fatal if missing (main.rs:897).
  2. Else CORECRUXD_EMBEDDING_URL setEmbeddingClient against an Ollama-compatible service. dimensions: 0 means auto-detect (main.rs:930).
  3. Else CORECRUXD_LOCAL_EMBEDDER (default on) → an in-process CPU embedder (main.rs:942). With the dense-embed-model feature and CORECRUXD_DENSE_MODEL=fastembed this is FastEmbedEmbedder, which downloads its model into the data dir on first use; on init failure it falls back to LocalHashEmbedder. Otherwise it is LocalHashEmbedder, pure Rust, always available, offline.
  4. If none apply, no embedder is configured and dense retrieval is inert.

4.6 The 14 background tasks

All subscribe to the same broadcast::Sender<()> shutdown channel created at main.rs:586.

#TaskSpawn gateIntervalLocation
1Routing reloaderalwaysCORECRUXD_ROUTING_RELOAD_INTERVAL_MS, default 1000main.rs:588
2Capacity guardCORECRUXD_CAPACITY_GUARD_ENABLED, default onCORECRUXD_CAPACITY_GUARD_INTERVAL_SECS, default 30, floored at 10smain.rs:620
3Update checkerinside spawn_update_checkerCORECRUXD_UPDATE_CHECK_INTERVAL_SECS, default 3600main.rs:637
4Ephemeral reserved-fact GCCORECRUXD_EPHEMERAL_GC, default off, read once at boot, toggling requires a restarthourlymain.rs:1054
5Consolidation review schedulerCORECRUXD_CONSOLIDATION_SCHEDULER, default offCORECRUXD_CONSOLIDATION_SCHEDULER_INTERVAL_SECS, default 3600main.rs:1061
6Near-duplicate router sweepa semantic dedup threshold is set15smain.rs:1080
7Session TTL reaperalways60smain.rs:1199
8Observation retentionCORECRUXD_OBS_RETENTION_DAYS above 0hourly, after a 30s initial delaymain.rs:1223
9Background fact sync (pull then push)sync enabled with a non-empty remote URLCORECRUXD_SYNC_INTERVAL_SECS, default 300, after a 5s initial delaymain.rs:1258
10Witness submission drainCORECRUXD_WITNESS_ENABLEDCORECRUXD_WITNESS_INTERVAL_SECS, default 300, floored at 1main.rs:1329
11SyncScheduler driver, hosts github-sync and the vault watcheralways spawned; jobs self-skipgithub-sync: CORECRUXD_GITHUB_SYNC_INTERVAL_SECS, default 900. Vault watcher: its own intervalmain.rs:1490
12Shutdown signal handleralways-main.rs:636
13Anonymous passport claim (one-shot, network)CORECRUXD_PASSPORT_CLAIM_ON_STARTUP, default ononcemain.rs:535
14daemon_start usage ping (one-shot, blocking pool)a three-way consent gateonce per bootmain.rs:1448

SyncScheduler job status is written as a fact under __sync__::<job_id> key status, readable through GET /v1/facts (main.rs:1487).

Task 13 is the one outbound call in a near-default configuration. CORECRUXD_PASSPORT_CLAIM_ON_STARTUP defaults to true (config.rs:890) and posts to https://passport.vaultcrux.com/v1/claim-anonymous (config.rs:16). It writes a passport.claimed marker under state_dir so it never retries. Set CORECRUXD_PASSPORT_CLAIM_ON_STARTUP=0 for an air-gapped or privacy-sensitive deployment. config.example.yaml:16 sets it to true, so an operator who copies the example gets outbound traffic.

4.7 Restart-recovery behaviour

Six things happen on a restart that are not obvious from the boot table.

BehaviourWhat it doesLocation
Incomplete repo scansAny scan still marked in progress from the previous run is marked failed with reason "daemon restarted before scan completed"main.rs:872
.ccxi index reloadSealed retrieval-index companions are rescanned from shards/*/segments/. Guarded on `config.build_ccxi \\config.local_ingest_enabled`: without this leg, local-ingest segments would not be served after a restartmain.rs:774
Relations replayrelations.jsonl is replayed into ProjectionStatemain.rs:663
Cost-report replayJournalled POST /v1/cost/report posts replay into the in-memory cost store, so attribution survives a restart. No-op unless the cost lens is onmain.rs:658
Witness proofs replaywitness_proofs.jsonl is replayedmain.rs:700
Repo watchersrepo_watch.start_existing_repos()main.rs:888

One thing that does not survive a restart: device-authorization grants. Pending grants and refresh credentials live in a process-local registry, and the source states so (auth_device.rs:29).

4.8 Shutdown

  1. spawn_shutdown_signal (main.rs:2168) selects on ctrl_c() and, on Unix only, SIGTERM. SIGTERM registration failure is a deliberate expect, justified in the source: "SIGTERM registration failure is fatal, daemon cannot shut down gracefully." On non-Unix only SIGINT is handled.
  2. With the otel feature, the batch span exporter is flushed before the broadcast fires (main.rs:2186).
  3. tx.send(()) broadcasts to every subscriber (main.rs:2189).
  4. Each HTTP listener runs axum::serve(...).with_graceful_shutdown(...) and arms a drain cap timer that starts only once draining begins (main.rs:2246). If the cap elapses first, the serve future is dropped and a warning is logged: graceful-shutdown drain cap exceeded; abandoning remaining connections to process exit. The cap comes from CORECRUXD_SHUTDOWN_DRAIN_SECS, default 30; 0 means drain forever.
  5. drop(lock_file) releases the LOCK flock (main.rs:1601).

The drain cap bounds how long shutdown blocks, not how long connections live. Connection tasks already spawned by axum keep running until process exit closes their sockets (main.rs:2226).

Every accepted connection on both HTTP listeners gets TCP_NODELAY (main.rs:2238); failure is logged at trace only.

Sources