Aurora: fully_consistent and at_least_as_fresh reads intermittently miss the immediately-preceding write.
Summary
at_least_as_fresh reads can miss a relationship even when the ZedToken passed is the written_at token returned directly from the write that created that exact relationship — i.e., a token that should, by definition, guarantee the read observes the write. This occurs when SpiceDB is configured with --datastore-read-replica-conn-uri, even when that replica URI points at the same Aurora instance as the primary (ruling out real replication lag as a cause). The same setup also produces misses under fully_consistent.
Environment
- SpiceDB version: v1.54.0
- Datastore: Amazon Aurora PostgreSQL
- Deployment: single SpiceDB instance
Relevant serve config
exec spicedb serve \
--http-enabled \
--grpc-preshared-key=$SPICEDB_GRPC_PRESHARED_KEY \
--datastore-engine=postgres \
--grpc-max-workers $MAX_WORKERS \
--dispatch-cache-max-cost=$DISPATCH_CACHE_MAX_COST \
--datastore-conn-pool-read-max-open $MAX_OPEN_CONNECTIONS \
--datastore-conn-uri=$DB_URL \
--skip-release-check=true \
--datastore-read-replica-conn-uri=$REPLICA_DB_URL \
--datastore-read-replica-conn-pool-max-open=$REPLICA_MAX_OPEN_CONNECTIONS \
--grpc-max-conn-age=10m \
--max-lookup-resources-limit=10000 \
--datastore-revision-quantization-interval=100ms \
--log-level=panic \
--log-format=none$DB_URL and $REPLICA_DB_URL are set to the same value in the environment where this was reproduced.
Reproduction (fully_consistent)
require 'securerandom'
resource_type = 'team'
relation = 'creator'
subject_type = 'workspace_user'
subject_id = SecureRandom.uuid
misses_fully_consistent = []
misses_at_least_as_fresh = []
client_options = {
target: 'auth:50051',
interceptors: [Authzed::GrpcUtil::BearerToken.new(token: 't_your_token_here_1234567deadbeef'), []].compact.flatten
}
# Only use insecure channel in development environment
client_options[:credentials] = :this_channel_is_insecure
client = Authzed::Api::V1::Client.new(**client_options)
(1..1000).each do |i|
resource_id = "res_#{i}"
update = Authzed::Api::V1::RelationshipUpdate.new(
operation: Authzed::Api::V1::RelationshipUpdate::Operation::OPERATION_TOUCH,
relationship: Authzed::Api::V1::Relationship.new(
resource: Authzed::Api::V1::ObjectReference.new(object_type: resource_type, object_id: resource_id),
relation: relation,
subject: Authzed::Api::V1::SubjectReference.new(
object: Authzed::Api::V1::ObjectReference.new(object_type: subject_type, object_id: subject_id)
)
)
)
write_response = client.permissions_service.write_relationships(
Authzed::Api::V1::WriteRelationshipsRequest.new(updates: [update])
)
token = write_response.written_at
# fully_consistent check
fc_hit = client.permissions_service.read_relationships(
Authzed::Api::V1::ReadRelationshipsRequest.new(
relationship_filter: Authzed::Api::V1::RelationshipFilter.new(
resource_type: resource_type,
optional_resource_id: resource_id,
optional_relation: relation
),
consistency: Authzed::Api::V1::Consistency.new(fully_consistent: true)
)
).any?
misses_fully_consistent << resource_id unless fc_hit
# at_least_as_fresh check, using the token from this exact write
af_hit = client.permissions_service.read_relationships(
Authzed::Api::V1::ReadRelationshipsRequest.new(
relationship_filter: Authzed::Api::V1::RelationshipFilter.new(
resource_type: resource_type,
optional_resource_id: resource_id,
optional_relation: relation
),
consistency: Authzed::Api::V1::Consistency.new(at_least_as_fresh: token)
)
).any?
misses_at_least_as_fresh << resource_id unless af_hit
end
puts "fully_consistent misses: #{misses_fully_consistent.size}/1000 -> #{misses_fully_consistent}"
puts "at_least_as_fresh misses: #{misses_at_least_as_fresh.size}/1000 -> #{misses_at_least_as_fresh}"
#### OUTPUT ####
fully_consistent misses: 13/1000 -> ["res_103", "res_138", "res_169", "res_194", "res_261", "res_341", "res_423", "res_583", "res_605", "res_695", "res_696", "res_747", "res_786"]
at_least_as_fresh misses: 13/1000 -> ["res_38", "res_86", "res_328", "res_349", "res_414", "res_496", "res_581", "res_697", "res_711", "res_762", "res_837", "res_902", "res_985"]
##############~13/1001 (~1.3%) misses, reproduced consistently. Underlying read call uses consistency: Authzed::Api::V1::Consistency.new(fully_consistent: true).
Diagnostics already performed
- Rerunning the read-only half of the loop afterward returns 100% hits — every write genuinely committed.
- Not CockroachDB (Aurora PostgreSQL) — clock-skew explanation doesn't apply.
- Not the known Postgres
HeadRevisionregression (#3127) — already fixed as of v1.53.0/v1.54.0. - Not multiple SpiceDB replicas, not a client-side connection-switching artifact — single SpiceDB instance, single reused gRPC channel.
Source: authzed/spicedb