clockworklabs/SpacetimeDB · error
ERROR: Skipping lifecycle reducer registration '%s' because
Error message
ERROR: Skipping lifecycle reducer registration '%s' because circular reference error is set
What it means
A lifecycle reducer (init, client_connected/on-connect, client_disconnected/on-disconnect) registration is skipped because g_circular_ref_error was set by an earlier type-cycle detection. RegisterLifecycleReducer is invoked from the ST_INIT/ST_CLIENT_CONNECTED/ST_CLIENT_DISCONNECTED macros during static init; when the module is already marked broken, the lifecycle hook is silently omitted and preinit_99 will fail the module. The root cause is the circular type reference, not the lifecycle reducer itself.
Source
Thrown at crates/bindings-cpp/include/spacetimedb/internal/v10_builder.h:400
p.elements.emplace_back(std::make_optional(param_name), std::move(internal_type));
}.template operator()<Is>(out_params, names, reg)), ...);
}(std::make_index_sequence<traits::arity - 1>{}, params, param_names, type_reg);
}
RawReducerDefV10 reducer_def{
reducer_name,
std::move(params),
FunctionVisibility::ClientCallable,
MakeUnitAlgebraicType(),
MakeStringAlgebraicType(),
};
UpsertReducer(reducer_def);
}
template<typename Func>
void RegisterLifecycleReducer(const std::string& reducer_name, Func func, Lifecycle lifecycle) {
if (g_circular_ref_error) {
std::fprintf(stderr, "ERROR: Skipping lifecycle reducer registration '%s' because circular reference error is set\n",
reducer_name.c_str());
return;
}
using traits = function_traits<Func>;
static_assert(traits::arity > 0, "Reducer must have at least one parameter (ReducerContext)");
if constexpr (traits::arity > 0) {
using FirstParamType = std::remove_cv_t<std::remove_reference_t<typename traits::template arg_t<0>>>;
static_assert(std::is_same_v<FirstParamType, ReducerContext>,
"First parameter of reducer must be ReducerContext");
}
std::function<void(ReducerContext&, BytesSource)> handler;
if constexpr (traits::arity == 1) {
handler = [func](ReducerContext& ctx, BytesSource) {
auto result = func(ctx);
if (result.is_err()) {
::SpacetimeDB::fail_reducer(result.error());
}View on GitHub (pinned to 524b4487d9)
Solutions
- Search stderr upwards for '[CIRCULAR REFERENCE DETECTED]' and fix that type first — the lifecycle reducer needs no change
- Replace embedded recursive members with ID references in the offending type
- Rebuild and verify the init/connect/disconnect reducers are present when the module publishes
- If stderr shows multiple skipped registrations, expect them all to clear at once once the cycle is gone
Example fix
// before — module has recursive type; ST_INIT registration is dropped
struct TreeNode { std::vector<TreeNode> children; };
ST_INIT(init_fn)(SpacetimeDB::ReducerContext&) { /* seed data */ }
// after
struct TreeNode { std::vector<uint32_t> child_ids; };
ST_INIT(init_fn)(SpacetimeDB::ReducerContext&) { /* seed data */ } Defensive patterns
Strategy: validation
Validate before calling
#include <spacetimedb/internal/module_type_registration.h>
TEST(Module, LifecycleReducersRegistered) {
EXPECT_FALSE(SpacetimeDB::Internal::g_circular_ref_error);
} Prevention
- Verify after publish that init/connect/disconnect hooks are present in the module description
- Keep lifecycle macro names and the module's types in one review pass — a schema change can silently drop hooks
- Assert the flag is false in tests to catch regressions early
When it happens
Trigger: A module contains both lifecycle macros and a self/mutually referential type anywhere in its typespace. Static init registers types first; the cycle sets the flag; the subsequent RegisterLifecycleReducer('<name>', ...) call for the init/connect/disconnect hook prints this message and returns without registering.
Common situations: Boilerplate init reducer in a project whose schema was just extended with a recursive struct; porting a Rust module to C++ bindings and keeping a recursive enum/struct shape; adding lifecycle hooks to a module that already failed type registration earlier in the log.
Related errors
- ERROR: Skipping multi-column index registration '%s.%s' beca
- ERROR: Skipping default-value registration '%s.%s' because c
- ERROR: Skipping reducer registration '%s' because circular r
- ERROR: Skipping view registration '%s' because circular refe
- ERROR: Skipping procedure registration '%s' because circular
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/5076eabcf00702c8.
Report an issue: GitHub.