IL-0432 Hello Inc. · 2026-10-27 (published)databasegcp
AutoReserve notifications silently went unsent as Memorystore for Redis 3.2 rejected writes with OOM below its limit
Respo and AutoReserve are one Ruby on Rails application that share the relational database and Redis, run on Google Cloud with Memorystore for Redis as the cache. Because one notification can be sent through several paths, the application recorded a sent status in Redis with Rails.cache.write using unless_exist: true and a 3-minute expiry, and skipped sending when the write did not succeed. Rails.cache.write returns false when the key already exists and nil when the write itself fails, and Rails does not raise an exception on a failed cache write. The code did not handle nil, so every failed write was treated as already sent and logged "Notification already scheduled". The author had assumed an exception would be raised, which slowed the investigation.
Two checks decide whether memory is short on Memorystore. Redis looks at its logical memory use, the total of the values it holds, and when that reaches its limit it evicts keys with an expiry and only then rejects writes. Memorystore looks at the physical memory of the Redis process plus the OS and management processes, and rejects writes regardless of Redis's own limit or eviction. The physical figure is not on the instance page and is visible with INFO memory. The 6.71 GiB on the instance page was logical use; physical use was 8.81 GiB, 1.31 times the logical figure. Redis saw free room and evicted nothing, while the instance's physical memory moved back and forth across the threshold, so writes were rejected now and then. The gap was memory fragmentation: jemalloc can return memory to the OS only in whole pages, freed space is reused only by values of the same size, and cache values of varying sizes kept adding pages. Google Cloud's documentation states that fragmentation can leave a Memorystore instance out of memory even when the used-memory ratio is low.
The team first scaled the instance from 8 GiB to 16 GiB, but writes were still rejected, because the cached data grew with the limit and the fragmentation grew with it. The rejections stopped when Redis 3.2 was upgraded to 7.2: logical use went from 12.45 GiB to 10.81 GiB, physical use from 17.45 GiB to 10.94 GiB, and the ratio from 1.40 to 1.01, because the new process laid the data out again. In about four months after the upgrade no write rejection was recorded, including periods when logical use reached the limit and keys were evicted, and the ratio stayed between 1.0 and 1.2. The team guesses that faster removal of expired keys in Redis 6.0 changed how free space appears, but has not confirmed what made the difference. The sent status was also moved from Redis to the relational database, using an UPDATE that sets notified_at only on a row where it is empty, so the one process whose update succeeds sends the notification.
- Severity
- –
- Scope
- One kind of AutoReserve reservation notification was intermittently not sent to users
- Users affected
- –
- Region
- –
- Time to detect
- –
- Time to resolve
- –
Symptoms
A bug report said that some reservation notifications in the AutoReserve restaurant reservation service never arrived. Of the several kinds of notification sent as a reservation progresses, only one kind sometimes arrived and sometimes did not.
The team first suspected the notification platform, but other notifications for the same reservation had arrived. The next suspicion was the restaurant's contract plan, but the send history showed mixed results within the same plan. The idea that the notification job was lost when the application restarted during a deploy was also checked and ruled out. The logs looked normal: they showed "Notification already scheduled" for the notifications that were never sent.
The missing notification was the only one whose processing used the Redis cache. A test write to Redis was refused with OOM command not allowed under OOM prevention, and the refusals came and went. The Redis instance was a Memorystore for Redis 8 GiB instance running version 3.2, and the memory usage on the instance page was 6.71 GiB, below the 8 GiB limit.
Symptom Writes rejectedMemory pressure
Root cause
Root cause Capacity exhaustion
- Trigger
- –
Detection
A bug report from AutoReserve said that some reservation notifications were not arriving.
Remediation
- Scaled the Memorystore for Redis instance from 8 GiB to 16 GiB, which did not stop the write rejections
- Upgraded Redis from 3.2 to 7.2, which removed the fragmentation and stopped the write rejections
- Moved the notification sent status from Redis to the relational database
Action items
- Consider enabling active defrag if physical memory use rises above 1.3 times logical memory use
Lessons
The logs and the instance page metrics did not show the cause; it took looking inside Redis with INFO memory to see physical memory far above logical memory. A cache is a place for values that can be rebuilt if lost and whose writes may fail, so the sent status, which had to persist and be decided once under concurrent processing, belonged in the relational database.
Sources
Related cases
IL-0290GitLab.com's Redis Sidekiq cluster ran out of memory and lost quorum, causing 500 errors and a 12 million job backlogGitLab · 2026 · sev1same root cause · shared In-memory cache
IL-058129CM product display servers lost pods and saturated Netty event loops on the first day of the Igu Week saleMUSINSA · 2025same root cause · shared In-memory cache
IL-0100PostHog's feature flags pods were packed onto Kubernetes nodes that were too small for their CPU requests, and CPU saturation above 90% caused three outages in ten days through Redis overload, connection pool exhaustion and slow queriesPostHog · 2025same root cause · shared In-memory cache
IL-0067Redis filled its persistent volume, corrupted its append-only file and crash-looped on every restartBC Government · 2025 · sev1same root cause · shared In-memory cache
IL-0544Wasabi's database cache stopped responding when its queueing service filled up, and S3 failed in all regionsWasabi · 2024same root cause · shared In-memory cache
IL-0202A burst of queries and database writes exhausted connection pools and memory on Honeycomb's ingest hosts, and Kubernetes crash-loop back-off kept failed hosts offline so ingest capacity stayed too low to recoverHoneycomb · 2023 · sev1same root cause · shared In-memory cache
IL-0386After a move to Google Cloud, Postgres connection timeouts and memcached i/o timeouts spiked on GKE nodesincident.io · 2023same root cause · shared In-memory cache
IL-0453Hakuna API latency spiked every day during the ElastiCache Redis backup as memory filled and the backup went forklessHyperconnect · 2022same root cause · shared In-memory cache
IL-0212Many Slack users could not connect because a Vitess keyspace for channel membership was overloaded by cache-miss queriesSlack · 2022same root cause · shared In-memory cache
IL-0297Mezmo users could not sign in and log indexing lagged after the auth cache's Redis servers were OOM killedMezmo · 2019same root cause · shared In-memory cache
IL-0520logs.cloud.gov trapped users in a login redirect loop because its full Redis session store stopped saving sessionscloud.gov · 2016same root cause · shared In-memory cache
IL-0296Transloadit's us-east Redis cluster lost queued jobs and heartbeats, raising WORKER_JOB_ERROR errors for 81 customersTransloadit · 2016same root cause · shared In-memory cache
Check your cluster for this
For AI tools and scripts
- This case as JSON: https://incidentlib-api.benchlet.com/cases/IL-0432
- The cases most like it: https://incidentlib-api.benchlet.com/cases/IL-0432/similar
- MCP server (Streamable HTTP, no key):
https://incidentlib-mcp.benchlet.com/mcp
The API needs no key. The MCP server offers search_incidents, get_incident, list_incidents, get_topic and vocab; both are explained on API and MCP.