Skip to content

Memorystore Redis Concurrency Limit Service

memorystore-redis-concurrency-limit is the GCP-managed MemoryStore for Redis instance (concurrency-limit-redis) that holds Sidekiq concurrency-limit deferred jobs. When a worker exceeds its concurrency limit, Gitlab::SidekiqMiddleware::ConcurrencyLimit::QueueManager pushes the serialized job onto a Redis LIST here instead of running it. ConcurrencyLimit::ResumeWorker drains those LISTs as capacity frees up.

Queue depth on this instance tracks worker throttling, not user traffic. It is idle whenever no worker is being throttled.

It is a managed MemoryStore instance, not a Sentinel-based Redis VM.

Deferred jobs previously lived on redis-cluster-shared-state. A throttled high-volume worker such as AuditEvents::AuditEventStreamingWorker can accumulate millions of serialized payloads, and that growth drove shared-state memory past 90%. Shared-state also holds distributed locks, MR diffs, and CI pipeline state, so an OOM there has a wide blast radius. Isolating this workload removes that vector.

See the epic: https://gitlab.com/groups/gitlab-com/gl-infra/tenant-scale/tenant-services/-/epics/72.

Rails reaches the instance through the concurrency_limit storage selector, wired by Gitlab::Redis::ConcurrencyLimit. The metrics-catalog definition is metrics-catalog/services/memorystore-redis-concurrency-limit.jsonnet.

Provisioning and sizing live in environments/gstg/memorystore.tf. The instance is STANDARD_HA with one replica, maxmemory-policy=noeviction, and persistence disabled.

Keys are per worker, sharded by worker name:

  • sidekiq:concurrency_limit:throttled_jobs:{worker_name} — the deferred-job LIST.
  • sidekiq:concurrency_limit:resume_meta:{worker_name} — resume metadata.
  • sidekiq:concurrency_limit:drop_requested:{worker_name} — admin purge flag.

Gitlab::ExclusiveLease still uses Gitlab::Redis::SharedState, so the concurrency_limit:queue_manager:{worker} lease keys remain on shared-state and are expected there.

For connecting with redis-cli, see connecting-to-memorystore-redis.md.

Under noeviction Redis rejects writes once memory is full rather than discarding keys. A deferred job is the only copy of that job, so eviction would lose work silently. Rejected writes surface as errors in the rails_redis_client SLI instead, which is the intended failure mode.

This is why the memory saturation alert is s2 at soft 0.40 / hard 0.50, defined by memorystore_redis_memory_tracechunks in libsonnet/saturation-monitoring/memorystore_redis_memory.libsonnet. The default memorystore_redis_memory point is s4 and does not page, which would be wrong for a workload where saturation means rejected writes.

The s2 saturation alert depends on the Stackdriver exporter relabeling concurrency-limit-redis to memorystore-redis-concurrency-limit. Until ArgoCD MR !3487 lands, the saturation query has no matching Stackdriver series and cannot page for this instance.

The service has two SLIs, each from a different metric source:

  • primary_server: operations on the Redis primary, from the GCP Stackdriver exporter (stackdriver_redis_instance_redis_googleapis_com_commands_calls). Reflects server-side activity on the managed instance.
  • rails_redis_client: all concurrency-limit operations issued from the Rails codebase (gitlab_redis_client_requests_total{storage="concurrency_limit"}). Reflects client-side activity.

Because the two SLIs have independent sources, comparing them distinguishes a real traffic stop from a metric-pipeline break: Rails traffic present while primary_server is absent points at the Stackdriver exporter, not Redis.

An absent traffic signal is benign here whenever no worker is throttled, in a way it is not for other Redis instances.

The Stackdriver primary_server and memory saturation signals remain unavailable until ArgoCD MR !3487 relabels the GCP instance name to this service type. The rails_redis_client signal also waits for work item 479.

SidekiqConcurrencyLimitQueueBacklogged fires on sidekiq_queue_size and is instance-agnostic, so it is unaffected by this migration. See sidekiq/sidekiq-concurrency-limit.md for how to respond to a backlog.