InterviewHack.ai
Empezar gratis
Blog/Preguntas de entrevista de Redis — 35 con código y respuestas reales

Preguntas de entrevista de Redis — 35 con código y respuestas reales

16 de septiembre de 2026

redisbackend-developer

35 preguntas de Redis para entrevistas backend y system design: estructuras de datos, pub/sub, persistencia, clustering, patrones de caché. Con ejemplos de código.

Redis aparece en prácticamente toda entrevista backend de nivel mid o senior. No importa si el rol es para una fintech, un SaaS o una empresa de ecommerce: si manejas datos a escala, alguien te va a preguntar cómo cacheas, cómo evitás race conditions o cómo diseñás un sistema de rate limiting. Esta guía tiene 35 preguntas reales, con el contexto de por qué las hacen, código funcional y el caso real donde eso aplica.


Estructuras de datos

1. ¿Cuáles son las estructuras de datos nativas de Redis y cuándo usás cada una?

Por qué la hacen: Quieren saber si conocés más allá del GET/SET básico. Si decís "Redis es un key-value store" y te quedás ahí, mal comienzo.

Respuesta:

| Estructura | Uso típico |

|---|---|

| String | Contadores, caché simple, flags, tokens de sesión |

| Hash | Objetos (perfil de usuario, producto) — mejor que JSON si actualizás campos individuales |

| List | Colas de trabajo, feeds cronológicos, historial limitado |

| Set | Tags únicos, follows, relaciones muchos-a-muchos |

| Sorted Set | Rankings, leaderboards, rate limiting por ventana deslizante |

| Stream | Log de eventos, message queue con grupos de consumidores |

| Bitmap | Presencia diaria, feature flags por usuario a escala masiva |

| HyperLogLog | Conteo aproximado de únicos (visitantes, IPs) con memoria fija |

python
import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# String — contador de vistas
r.incr('post:42:views')

# Hash — perfil de usuario
r.hset('user:1001', mapping={
    'name': 'Martín',
    'plan': 'pro',
    'interviews_done': 3
})
print(r.hget('user:1001', 'plan'))  # 'pro'

# List — cola FIFO
r.rpush('jobs:queue', 'job:a', 'job:b')
job = r.blpop('jobs:queue', timeout=5)

# Set — seguidores
r.sadd('user:1001:following', 'user:2', 'user:3')
r.sadd('user:2:following', 'user:1001', 'user:4')

# Usuarios que los dos siguen
common = r.sinter('user:1001:following', 'user:2:following')

# Sorted Set — leaderboard
r.zadd('ranking:global', {'user:1001': 9850, 'user:2': 7200})
top3 = r.zrevrange('ranking:global', 0, 2, withscores=True)

Caso real: En una app de entrevistas técnicas, usamos Sorted Set para el leaderboard de práctica (score = puntos acumulados) y Bitmap para marcar qué días del mes el usuario practicó, lo que nos permite calcular rachas semanales sin leer filas de base de datos.


2. ¿Cuándo usás un Hash en lugar de serializar JSON en un String?

Por qué la hacen: Hay ingenieros que guardan todo como JSON string. No es siempre incorrecto, pero tiene trade-offs.

Respuesta:

Usás Hash cuando:

  • Necesitás actualizar campos individuales del objeto sin reescribir todo
  • Querés explotar HGETALL, HMGET para lectura parcial
  • El objeto tiene muchos campos pero sólo leés algunos en cada request

Usás String con JSON cuando:

  • El objeto es inmutable (lo cacheás completo y lo invalidás completo)
  • Necesitás usarlo en scripts Lua o en pipelines sin lógica de campo
  • La serialización ya existe y la latencia de parsear es despreciable
python
# MAL: actualizar un campo en JSON requiere leer-parsear-escribir
raw = r.get('user:1001')
user = json.loads(raw)
user['interviews_done'] += 1
r.set('user:1001', json.dumps(user))

# BIEN: con Hash es atómico
r.hincrby('user:1001', 'interviews_done', 1)

Trampa de entrevista: los Hashes con muchos campos (>128 por defecto) pasan de ziplist a hashtable internamente. Para objetos muy pequeños, el ziplist encoding usa menos memoria que tener una key separada por campo. El entrevistador puede preguntarte sobre hash-max-listpack-entries.


3. Explicá la diferencia entre List, Set y Sorted Set. ¿En qué escenario usarías cada uno para un feed de actividad?

Por qué la hacen: Es una pregunta de diseño disfrazada de pregunta de estructura de datos.

Respuesta:

  • List: ordenada por inserción, permite duplicados, acceso O(1) en extremos, O(N) en el medio
  • Set: no ordenada, sin duplicados, operaciones de conjuntos (intersección, unión) muy eficientes
  • Sorted Set: ordenada por score, sin duplicados de member, O(log N) en inserciones y rangos

Para un feed de actividad:

python
import time

# Feed cronológico de un usuario — Sorted Set con timestamp como score
def add_to_feed(user_id: str, activity_id: str):
    key = f'feed:{user_id}'
    score = time.time()
    r.zadd(key, {activity_id: score})
    # Mantener sólo los últimos 200 items
    r.zremrangebyrank(key, 0, -201)

def get_feed(user_id: str, page: int = 0, per_page: int = 20):
    key = f'feed:{user_id}'
    start = page * per_page
    end = start + per_page - 1
    return r.zrevrange(key, start, end, withscores=True)

Usarías List si el feed es estrictamente FIFO/LIFO y nunca necesitás rango por tiempo. Usarías Set si solo te importa "¿este item ya apareció?" (deduplicación). En la práctica, el Sorted Set con timestamp es la solución más flexible.


4. ¿Qué son los Streams de Redis y cómo los comparás con Kafka?

Por qué la hacen: Los Streams aparecieron en Redis 5.0 y hay equipos que los usan como alternativa liviana a Kafka.

Respuesta:

Redis Streams es una estructura de log append-only con IDs de secuencia, grupos de consumidores y ACK. A diferencia de una List, los mensajes persisten aunque sean consumidos (los borrás explícitamente o por trim).

python
# Productor
r.xadd('events:interviews', {
    'user_id': '1001',
    'event': 'session_started',
    'company': 'Mercado Libre',
    'ts': str(time.time())
})

# Consumidor con grupo
r.xgroup_create('events:interviews', 'analytics-group', id='0', mkstream=True)

while True:
    messages = r.xreadgroup(
        groupname='analytics-group',
        consumername='worker-1',
        streams={'events:interviews': '>'},
        count=10,
        block=2000  # ms
    )
    for stream, entries in (messages or []):
        for entry_id, fields in entries:
            process(fields)
            r.xack('events:interviews', 'analytics-group', entry_id)

Diferencias clave con Kafka:

| | Redis Streams | Kafka |

|---|---|---|

| Retención | Limitada (MAXLEN), en RAM | Disco, configurable (días/TB) |

| Throughput | ~100k msg/s por instancia | Millones msg/s en cluster |

| Latencia | Sub-ms | ~2-10ms típico |

| Replicación | Replication simple, no particionado | Particiones y réplicas independientes |

| Caso ideal | Eventos internos, cola liviana | Event sourcing, analytics a escala |


Persistencia: RDB vs AOF

5. Explicá RDB y AOF. ¿Cuándo usarías cada uno?

Por qué la hacen: Cualquier sistema productivo necesita durabilidad. Si no sabés los trade-offs, no estás listo para operar Redis en producción.

Respuesta:

RDB (Redis Database Backup): snapshots periódicos del dataset completo a disco. Redis hace un fork() del proceso, el hijo escribe el archivo .rdb y el padre sigue sirviendo. El archivo es compacto y la restauración es rápida.

AOF (Append Only File): cada escritura se loguea en un archivo. En restart, Redis rehidrata el estado reproduciendo las operaciones. Opciones de fsync: always (durabilidad máxima, I/O alto), everysec (default, pérdida máxima de 1 segundo), no (deja el OS decidir).

bash
# redis.conf — configuración recomendada para producción
save 900 1       # RDB: si hubo 1 cambio en 900s
save 300 10      # RDB: si hubo 10 cambios en 300s
save 60 10000    # RDB: si hubo 10000 cambios en 60s

appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

¿Cuándo usar cuál?

  • Solo RDB: si podés tolerar pérdida de minutos de datos (caché puro, session store donde el impacto es bajo)
  • Solo AOF: si necesitás durabilidad máxima y no te importa la velocidad de restart
  • RDB + AOF (recomendado en producción): Redis usa AOF para recovery (más reciente) y RDB para backups/snapshots

Dato que impresiona al entrevistador: el AOF rewrite usa un archivo temporal para compactar la secuencia de operaciones. Podés forzarlo con BGREWRITEAOF. En deployments con mucha escritura, el AOF puede crecer a gigabytes si no configurás auto-aof-rewrite-percentage.


6. ¿Qué pasa con la durabilidad si Redis cae entre dos snapshots RDB?

Por qué la hacen: Testean si entendés el compromiso fundamental de RDB.

Respuesta: Perdés todos los datos escritos desde el último snapshot. Si configuraste save 60 10000, en el peor caso perdés hasta 60 segundos de operaciones. Para muchos casos de uso (caché, rate limiting, sesiones) esto es aceptable. Para un carrito de compras o una transacción financiera, no.

La solución es AOF con appendfsync everysec que reduce la ventana de pérdida a ~1 segundo, o appendfsync always para cero pérdida (con costo de latencia).

python
# Cómo validar el estado de persistencia desde código
info = r.info('persistence')
print(f"RDB last save: {info['rdb_last_save_time']}")
print(f"AOF enabled: {info['aof_enabled']}")
print(f"AOF pending rewrite: {info['aof_rewrite_scheduled']}")

7. ¿Qué es el "fork bomb" de Redis y cómo lo prevenís?

Por qué la hacen: Pregunta de nivel senior. Demuestra que entendés el modelo de proceso de Redis.

Respuesta: Redis usa fork() para RDB y AOF rewrite. En Linux, fork() es copy-on-write: el hijo comparte páginas de memoria con el padre. Pero si el padre escribe mucho durante el fork (alta tasa de escritura), el kernel tiene que copiar páginas modificadas — esto puede duplicar el uso de memoria momentáneamente y causar swap, matando la latencia.

Mitigaciones:

  1. 1vm.overcommit_memory = 1 en el kernel (permite al fork de Redis asignar memoria que "en teoría" no existe)
  2. 2Deshabilitar Transparent Huge Pages: echo never > /sys/kernel/mm/transparent_hugepage/enabled (los THP amplifican el copy-on-write)
  3. 3Monitorear used_memory vs used_memory_rss en INFO memory — si el RSS es mucho mayor que used_memory, hay fragmentación o CoW en progreso

Pub/Sub y Streams

8. ¿Cuál es la diferencia entre Pub/Sub y Streams? ¿Cuándo usás cada uno?

Por qué la hacen: Son dos mecanismos de messaging muy distintos que se confunden.

Respuesta:

Pub/Sub: fire-and-forget. Si no hay suscriptores en el momento de la publicación, el mensaje se pierde. No hay persistencia, no hay ACK, no hay history.

python
# Pub/Sub — notificaciones en tiempo real
import threading

def subscriber():
    sub = redis.Redis(decode_responses=True)
    p = sub.pubsub()
    p.subscribe('notifications:user:1001')
    for message in p.listen():
        if message['type'] == 'message':
            print(f"Notificación: {message['data']}")

# En otro thread/proceso
thread = threading.Thread(target=subscriber, daemon=True)
thread.start()

r.publish('notifications:user:1001', 'Nueva oferta de trabajo disponible')

Streams: log persistente con grupos de consumidores, ACK, y posibilidad de releer desde cualquier offset.

Regla práctica:

  • Usá Pub/Sub para notificaciones en tiempo real donde la pérdida es aceptable (live chat, dashboards, invalidación de caché distribuida)
  • Usá Streams para eventos que no podés perder y que múltiples workers necesitan procesar (procesamiento de pagos, analytics, jobs queue con retry)

9. ¿Cómo implementarías invalidación de caché en múltiples instancias usando Pub/Sub?

Por qué la hacen: Sistema design real. Múltiples réplicas de tu app tienen caché local (in-memory) y necesitás invalidarlas cuando la data cambia.

Respuesta:

python
import threading
from functools import lru_cache

local_cache = {}

def setup_cache_invalidation_listener(redis_client):
    """Listener que corre en background thread"""
    def listen():
        sub = redis_client.pubsub()
        sub.subscribe('cache:invalidations')
        for message in sub.listen():
            if message['type'] == 'message':
                key = message['data']
                local_cache.pop(key, None)
                print(f"Cache local invalidado: {key}")
    
    thread = threading.Thread(target=listen, daemon=True)
    thread.start()

def get_user(user_id: str) -> dict:
    cache_key = f'user:{user_id}'
    
    # L1: caché local en memoria
    if cache_key in local_cache:
        return local_cache[cache_key]
    
    # L2: Redis
    raw = r.get(cache_key)
    if raw:
        user = json.loads(raw)
        local_cache[cache_key] = user
        return user
    
    # L3: base de datos
    user = db.fetch_user(user_id)
    r.setex(cache_key, 3600, json.dumps(user))
    local_cache[cache_key] = user
    return user

def update_user(user_id: str, data: dict):
    db.update_user(user_id, data)
    cache_key = f'user:{user_id}'
    r.delete(cache_key)
    # Notificar a todas las instancias de la app
    r.publish('cache:invalidations', cache_key)

Redis Cluster y Sentinel

10. ¿Qué es Redis Sentinel y para qué sirve?

Por qué la hacen: Alta disponibilidad es básico en producción.

Respuesta: Sentinel es el sistema de monitoreo y failover automático de Redis para topologías master-replica. Corre como proceso separado y hace tres cosas:

  1. 1Monitoring: verifica constantemente que master y réplicas estén vivos
  2. 2Notification: alerta cuando algo falla (vía Pub/Sub o callback)
  3. 3Automatic failover: si el master cae, Sentinel elije una réplica y la promueve a master. Notifica a los clientes del nuevo master.
python
from redis.sentinel import Sentinel

sentinel = Sentinel(
    [('sentinel-1', 26379), ('sentinel-2', 26379), ('sentinel-3', 26379)],
    socket_timeout=0.1
)

# El cliente se conecta siempre al master actual
master = sentinel.master_for('mymaster', socket_timeout=0.1, decode_responses=True)
# Para lecturas puede usar réplicas
slave = sentinel.slave_for('mymaster', socket_timeout=0.1, decode_responses=True)

master.set('key', 'value')
print(slave.get('key'))

Cuándo NO usar Sentinel: cuando necesitás sharding horizontal. Sentinel te da HA pero no te escala horizontalmente. Para eso usás Redis Cluster.


11. Explicá Redis Cluster. ¿Cómo se distribuyen las claves?

Por qué la hacen: Escala horizontal es el corazón del system design a gran escala.

Respuesta: Redis Cluster distribuye los datos en 16384 hash slots. Cada nodo master es responsable de un subconjunto de slots. La clave de distribución es CRC16(key) % 16384.

Nodo A: slots 0-5460
Nodo B: slots 5461-10922
Nodo C: slots 10923-16383

Cada master tiene una o más réplicas para HA.

python
from redis.cluster import RedisCluster

# El cliente maneja el routing automáticamente
rc = RedisCluster(
    host='node-1',
    port=7000,
    decode_responses=True
)

rc.set('user:1001', 'data')  # va al nodo que corresponde según el hash slot

# Hash tags: forzar que múltiples keys estén en el mismo slot
rc.set('{user:1001}.profile', 'data')
rc.set('{user:1001}.sessions', 'data')
# Ambas van al slot de "user:1001" — necesario para operaciones multi-key

Limitación importante: las operaciones multi-key (MGET, MSET, transacciones) sólo funcionan si todas las keys están en el mismo slot. Por eso existen los hash tags {}: el CRC16 se calcula sólo sobre la parte entre llaves.


12. ¿Qué pasa si un nodo de Redis Cluster se cae?

Por qué la hacen: Testean que entendés el modelo de fallos.

Respuesta:

  1. 1Los otros nodos detectan la falla con el protocolo gossip (PING/PONG)
  2. 2Si el nodo caído tiene réplicas, se inicia un proceso de elección: la réplica con la data más actualizada se promueve a master
  3. 3El cluster remapea los hash slots al nuevo master
  4. 4Los clientes reciben errores MOVED o CLUSTERDOWN mientras dura el failover (~1-5 segundos)

Si el nodo caído no tiene réplicas, esos hash slots quedan inaccesibles y el cluster puede ponerse en estado FAIL dependiendo de la configuración de cluster-require-full-coverage.

bash
# Monitorear el estado del cluster
redis-cli --cluster check node-1:7000

# Ver la distribución de slots
redis-cli -h node-1 -p 7000 CLUSTER NODES

Patrones de invalidación de caché

13. Explicá los patrones Cache-Aside, Write-Through y Write-Behind.

Por qué la hacen: Son fundamentales en system design. Aparecen en entrevistas de nivel senior casi siempre.

Respuesta:

Cache-Aside (Lazy Loading): la app lee de caché primero. Si no está (cache miss), lee de DB y lo guarda en caché.

python
def get_product(product_id: str) -> dict:
    key = f'product:{product_id}'
    cached = r.get(key)
    if cached:
        return json.loads(cached)
    
    product = db.get_product(product_id)
    r.setex(key, 3600, json.dumps(product))
    return product

Pro: sólo cacheás lo que se usa. Con: primer request es lento (cold start), posible stale data.

Write-Through: cada escritura a la DB también actualiza el caché.

python
def update_product(product_id: str, data: dict):
    db.update_product(product_id, data)
    key = f'product:{product_id}'
    r.setex(key, 3600, json.dumps(data))  # siempre fresco

Pro: el caché siempre está al día. Con: escribís data que quizás nadie lee (write amplification).

Write-Behind (Write-Back): la app escribe al caché y la sincronización a DB es asíncrona.

python
def update_product_fast(product_id: str, data: dict):
    key = f'product:{product_id}'
    r.setex(key, 3600, json.dumps(data))
    # Encolar para persistencia asíncrona
    r.rpush('db:writes:queue', json.dumps({'table': 'products', 'id': product_id, 'data': data}))

Pro: latencia de escritura mínima. Con: si Redis cae antes de sincronizar a DB, perdés datos.


14. ¿Qué es un cache stampede y cómo lo prevenís?

Por qué la hacen: Problema clásico que rompe sistemas productivos bajo alta concurrencia.

Respuesta: Ocurre cuando una clave popular expira y múltiples requests simultáneos van a la DB al mismo tiempo para regenerarla. Si tenés 1000 requests/segundo y la clave expira, potencialmente 1000 queries golpean tu DB en el mismo momento.

Solución 1: Mutex lock

python
import uuid

def get_with_lock(key: str, regenerate_fn, ttl: int = 3600):
    value = r.get(key)
    if value:
        return json.loads(value)
    
    lock_key = f'lock:{key}'
    lock_value = str(uuid.uuid4())
    
    # Intentar tomar el lock (NX = solo si no existe)
    acquired = r.set(lock_key, lock_value, nx=True, ex=10)
    
    if acquired:
        try:
            data = regenerate_fn()
            r.setex(key, ttl, json.dumps(data))
            return data
        finally:
            # Liberar el lock sólo si somos nosotros quien lo tiene
            lua_script = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
            """
            r.eval(lua_script, 1, lock_key, lock_value)
    else:
        # Otro worker está regenerando, esperar y reintentar
        import time
        time.sleep(0.1)
        return get_with_lock(key, regenerate_fn, ttl)

Solución 2: Probabilistic early expiration (XFetch)

python
import math
import random
import time

def get_with_xfetch(key: str, regenerate_fn, ttl: int = 3600, beta: float = 1.0):
    """
    Probabilidad de revalidar aumenta a medida que se acerca la expiración.
    No necesita locks.
    """
    data = r.get(f'data:{key}')
    expiry = r.get(f'expiry:{key}')
    
    if data and expiry:
        remaining = float(expiry) - time.time()
        recompute_time = r.get(f'recompute_time:{key}') or '1'
        
        # Decidir probabilísticamente si recomputar anticipadamente
        if remaining - beta * float(recompute_time) * math.log(random.random()) > 0:
            return json.loads(data)
    
    start = time.time()
    new_data = regenerate_fn()
    recompute_time = time.time() - start
    
    r.setex(f'data:{key}', ttl, json.dumps(new_data))
    r.set(f'expiry:{key}', time.time() + ttl)
    r.set(f'recompute_time:{key}', recompute_time)
    
    return new_data

15. ¿Qué es cache penetration y cómo lo solucionás?

Por qué la hacen: Ataque/antipatrón frecuente en sistemas con usuarios maliciosos o datos incompletos.

Respuesta: Cache penetration ocurre cuando se consultan keys que no existen ni en caché ni en DB. Cada request pasa por caché (miss) y golpea la DB (también miss). Un atacante puede hacer esto intencionalmente con IDs inexistentes.

Solución 1: Cachear el resultado nulo

python
def get_user_safe(user_id: str):
    key = f'user:{user_id}'
    cached = r.get(key)
    
    if cached is not None:
        if cached == 'NULL':
            return None  # sabemos que no existe
        return json.loads(cached)
    
    user = db.get_user(user_id)
    if user:
        r.setex(key, 3600, json.dumps(user))
    else:
        r.setex(key, 300, 'NULL')  # TTL corto para nulos
    
    return user

Solución 2: Bloom Filter

python
# Con RedisBloom (módulo) o implementación propia
# Antes de consultar DB, verificar si el ID podría existir

from pybloom_live import BloomFilter

# Inicializar con todos los IDs existentes (en startup o batch)
bloom = BloomFilter(capacity=1000000, error_rate=0.01)
for user_id in db.get_all_user_ids():
    bloom.add(user_id)

def get_user_with_bloom(user_id: str):
    if user_id not in bloom:
        return None  # definitivamente no existe — sin DB query
    
    key = f'user:{user_id}'
    cached = r.get(key)
    if cached:
        return json.loads(cached)
    
    user = db.get_user(user_id)
    if user:
        r.setex(key, 3600, json.dumps(user))
        bloom.add(user_id)
    return user

Rate Limiting

16. ¿Cómo implementás rate limiting con Redis?

Por qué la hacen: Es uno de los casos de uso más comunes de Redis en producción. Hay varias estrategias con trade-offs distintos.

Respuesta:

Algoritmo 1: Fixed Window Counter

python
def is_allowed_fixed_window(user_id: str, limit: int = 100, window_seconds: int = 60) -> bool:
    key = f'ratelimit:{user_id}:{int(time.time()) // window_seconds}'
    current = r.incr(key)
    if current == 1:
        r.expire(key, window_seconds * 2)
    return current <= limit

Pro: simple, O(1). Con: permite burst al cambio de ventana (si hacés 100 requests al final de la ventana y 100 al inicio de la siguiente, pasaste 200 en 2 segundos).

Algoritmo 2: Sliding Window Log

python
def is_allowed_sliding_window(user_id: str, limit: int = 100, window_ms: int = 60000) -> bool:
    key = f'ratelimit:log:{user_id}'
    now = int(time.time() * 1000)
    window_start = now - window_ms
    
    pipe = r.pipeline()
    pipe.zremrangebyscore(key, 0, window_start)   # eliminar requests viejos
    pipe.zadd(key, {str(now) + str(uuid.uuid4()): now})  # agregar el request actual
    pipe.zcard(key)                                # contar requests en la ventana
    pipe.expire(key, window_ms // 1000 + 1)
    results = pipe.execute()
    
    return results[2] <= limit

Pro: ventana perfectamente deslizante. Con: usa memoria por cada request individual.

Algoritmo 3: Token Bucket con Lua (recomendado para producción)

lua
-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])      -- tokens por segundo
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local last_time = tonumber(redis.call('hget', key, 'last_time') or now)
local tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)

-- Calcular tokens generados desde la última vez
local elapsed = now - last_time
local new_tokens = math.min(capacity, tokens + elapsed * rate)

if new_tokens >= requested then
    redis.call('hset', key, 'tokens', new_tokens - requested, 'last_time', now)
    redis.call('expire', key, math.ceil(capacity / rate) + 1)
    return 1  -- permitido
else
    redis.call('hset', key, 'tokens', new_tokens, 'last_time', now)
    return 0  -- rechazado
end
python
with open('token_bucket.lua', 'r') as f:
    script = f.read()

token_bucket = r.register_script(script)

def is_allowed_token_bucket(user_id: str, capacity: int = 100, rate: float = 10) -> bool:
    result = token_bucket(
        keys=[f'bucket:{user_id}'],
        args=[capacity, rate, time.time(), 1]
    )
    return bool(result)

17. ¿Cómo harías rate limiting por IP y por usuario simultáneamente?

Por qué la hacen: En producción querés proteger tanto a nivel de IP (ataques) como de usuario (abuso de API).

Respuesta:

python
def check_rate_limits(user_id: str, ip: str, endpoint: str) -> tuple[bool, str]:
    """
    Retorna (is_allowed, reason)
    """
    limits = [
        # (key, limit, window)
        (f'rl:ip:{ip}', 1000, 60),           # 1000 req/min por IP
        (f'rl:user:{user_id}', 100, 60),      # 100 req/min por usuario
        (f'rl:user:{user_id}:{endpoint}', 10, 60),  # 10 req/min por endpoint
    ]
    
    pipe = r.pipeline()
    now = int(time.time())
    
    for key, limit, window in limits:
        window_key = f'{key}:{now // window}'
        pipe.incr(window_key)
        pipe.expire(window_key, window * 2)
    
    results = pipe.execute()
    counts = results[::2]  # cada segundo resultado es del expire, ignoramos
    
    for i, (key, limit, window) in enumerate(limits):
        if counts[i] > limit:
            return False, f'Rate limit exceeded: {key}'
    
    return True, 'ok'

Locks distribuidos

18. ¿Cómo implementás un distributed lock con Redis?

Por qué la hacen: Problema clásico de sistemas distribuidos. La implementación naive tiene bugs sutiles.

Respuesta:

Implementación correcta con SET NX EX:

python
import uuid
import time

class RedisLock:
    def __init__(self, redis_client, key: str, timeout: int = 10):
        self.r = redis_client
        self.key = f'lock:{key}'
        self.timeout = timeout
        self.token = str(uuid.uuid4())
        
    def acquire(self, blocking: bool = True, blocking_timeout: float = 5.0) -> bool:
        deadline = time.time() + blocking_timeout
        while True:
            acquired = self.r.set(
                self.key,
                self.token,
                nx=True,   # solo si no existe
                ex=self.timeout
            )
            if acquired:
                return True
            if not blocking or time.time() >= deadline:
                return False
            time.sleep(0.01)
    
    def release(self):
        """Libera el lock SOLO si somos el dueño — usa Lua para atomicidad"""
        lua = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """
        return self.r.eval(lua, 1, self.key, self.token)
    
    def __enter__(self):
        if not self.acquire():
            raise TimeoutError(f'No se pudo adquirir el lock {self.key}')
        return self
    
    def __exit__(self, *args):
        self.release()

# Uso
with RedisLock(r, 'invoice:42:process', timeout=30) as lock:
    process_invoice(42)

Por qué el check + delete debe ser atómico: si entre el GET (verificar que sos el dueño) y el DEL expira el lock y otro proceso lo adquiere, borrás el lock de otro. El script Lua ejecuta atómicamente.


19. ¿Qué es Redlock y cuándo lo necesitás?

Por qué la hacen: Martin Kleppmann y Salvatore Sanfilippo tuvieron un debate famoso sobre esto. Los entrevistadores senior lo conocen.

Respuesta: Redlock es el algoritmo de lock distribuido de Redis para múltiples instancias independientes (no réplicas). La idea: para adquirir un lock, tenés que adquirirlo en la mayoría (N/2 + 1) de N instancias Redis independientes dentro de un tiempo límite.

python
# Con la librería redlock-py
from redlock import Redlock

dlm = Redlock([
    {"host": "redis-1", "port": 6379},
    {"host": "redis-2", "port": 6379},
    {"host": "redis-3", "port": 6379},
])

lock = dlm.lock("my-resource", 10000)  # TTL en ms
if lock:
    try:
        # sección crítica
        do_work()
    finally:
        dlm.unlock(lock)

La controversia: Kleppmann argumenta que Redlock no es seguro porque no tiene en cuenta el clock drift y los procesos que se pausan (GC pause, swap). Para locks de seguridad (donde el sistema se rompe si dos procesos ejecutan simultáneamente), sugiere usar ZooKeeper o un consensus system real. Para locks de eficiencia (donde la ejecución duplicada es costosa pero no catastrófica), Redlock está bien.

Regla práctica: si tu lock protege recursos financieros o datos críticos, usá ZooKeeper/etcd. Para invalidar caché o serializar jobs no críticos, Redlock alcanza.


Pipelines y transacciones

20. ¿Qué es un pipeline en Redis y cuándo lo usás?

Por qué la hacen: La performance de Redis se degrada si hacés muchos round-trips. Pipelines es la optimización más fácil.

Respuesta: Un pipeline agrupa múltiples comandos y los envía en un solo round-trip. Redis los ejecuta secuencialmente y retorna todas las respuestas juntas.

python
import time

# Sin pipeline: N round-trips
start = time.time()
for i in range(1000):
    r.set(f'key:{i}', f'value:{i}')
print(f"Sin pipeline: {time.time() - start:.3f}s")

# Con pipeline: 1 round-trip
start = time.time()
pipe = r.pipeline(transaction=False)
for i in range(1000):
    pipe.set(f'key:{i}', f'value:{i}')
pipe.execute()
print(f"Con pipeline: {time.time() - start:.3f}s")
# Típicamente 10-50x más rápido en red con latencia

# Pipeline en Node.js
const pipeline = redis.pipeline();
for (let i = 0; i < 1000; i++) {
  pipeline.set(`key:${i}`, `value:${i}`);
}
await pipeline.exec();

Diferencia con transacción: el pipeline no garantiza atomicidad. Si un comando falla, los otros igualmente se ejecutan. Tampoco aísla de otros clientes que escriban mientras el pipeline procesa.


21. ¿Qué son MULTI/EXEC y cómo funcionan las transacciones en Redis?

Por qué la hacen: Las transacciones de Redis son distintas a las de SQL. Hay que entender qué garantizan y qué no.

Respuesta:

python
# MULTI/EXEC: todos los comandos entre MULTI y EXEC se ejecutan atómicamente
pipe = r.pipeline(transaction=True)  # transaction=True habilita MULTI/EXEC

pipe.multi()
pipe.incr('counter:views')
pipe.incr('counter:unique_users')
pipe.expire('counter:views', 3600)
results = pipe.execute()

# En Node.js
const results = await redis
  .multi()
  .incr('counter:views')
  .incr('counter:unique_users')
  .expire('counter:views', 3600)
  .exec();

Lo que MULTI/EXEC garantiza:

  • Atomicidad de ejecución: nadie puede intercalar comandos entre los del MULTI/EXEC
  • Aislamiento: otros clientes no ven estados intermedios

Lo que NO garantiza:

  • No hay rollback si un comando falla durante EXEC (si un comando tiene error de tipo, los otros igualmente se ejecutan)
  • No hay lectura-modificación-escritura atómica (para eso necesitás WATCH o Lua)

22. ¿Qué es WATCH y cómo implementás optimistic locking?

Por qué la hacen: El patrón check-then-act es una race condition clásica. WATCH es la solución de Redis.

Respuesta:

python
def transfer_points(from_user: str, to_user: str, amount: int, retries: int = 3):
    """
    Transferir puntos entre usuarios de forma segura con optimistic locking
    """
    from_key = f'points:{from_user}'
    to_key = f'points:{to_user}'
    
    for attempt in range(retries):
        with r.pipeline() as pipe:
            try:
                # WATCH: si alguna de estas keys cambia antes del EXEC, la transacción falla
                pipe.watch(from_key, to_key)
                
                from_points = int(pipe.get(from_key) or 0)
                if from_points < amount:
                    raise ValueError("Puntos insuficientes")
                
                # Iniciar la transacción
                pipe.multi()
                pipe.decrby(from_key, amount)
                pipe.incrby(to_key, amount)
                pipe.execute()
                
                return True  # éxito
                
            except redis.WatchError:
                # Alguien modificó las keys mientras calculábamos — reintentar
                if attempt == retries - 1:
                    raise
                time.sleep(0.01 * (2 ** attempt))  # backoff exponencial
    
    return False

23. ¿Cuándo usás scripts Lua en vez de MULTI/EXEC?

Por qué la hacen: Lua en Redis es una herramienta poderosa que pocos conocen bien.

Respuesta: Usás Lua cuando necesitás lógica condicional atómica: leer un valor, evaluarlo, y escribir según el resultado — todo sin que otro cliente pueda intervenir entre esas operaciones.

python
# Incrementar un contador SOLO si no supera un límite — no es posible con MULTI/EXEC
increment_if_under = r.register_script("""
local current = redis.call('GET', KEYS[1])
local count = tonumber(current) or 0
local limit = tonumber(ARGV[1])

if count < limit then
    redis.call('INCR', KEYS[1])
    redis.call('EXPIRE', KEYS[1], ARGV[2])
    return count + 1
else
    return -1
end
""")

result = increment_if_under(keys=['api:calls:user:1001'], args=[100, 3600])
if result == -1:
    raise Exception("Rate limit exceeded")

# Otro ejemplo: pop atómico de una lista con condición
conditional_pop = r.register_script("""
local item = redis.call('LINDEX', KEYS[1], 0)
if item and cjson.decode(item)['priority'] == ARGV[1] then
    return redis.call('LPOP', KEYS[1])
end
return nil
""")

Ventajas de Lua vs WATCH/MULTI:

  • No necesita retries por WatchError
  • Lógica compleja en una sola operación atómica
  • EVALSHA permite cachear el script compilado (envía hash en lugar del script completo)

Optimización de memoria

24. ¿Cómo monitoreás y optimizás el uso de memoria en Redis?

Por qué la hacen: Redis es in-memory; quedarse sin RAM es catastrófico.

Respuesta:

bash
# Comandos clave para diagnóstico
redis-cli INFO memory
redis-cli MEMORY USAGE user:1001     # bytes que usa una key específica
redis-cli MEMORY DOCTOR               # análisis automático con recomendaciones
redis-cli --bigkeys                   # encontrar las keys más grandes
redis-cli --hotkeys                   # keys con más accesos (requiere maxmemory-policy LFU)
python
# Análisis programático
info = r.info('memory')
print(f"Memoria usada: {info['used_memory_human']}")
print(f"Memoria RSS: {info['used_memory_rss_human']}")
print(f"Fragmentación: {info['mem_fragmentation_ratio']:.2f}")
print(f"Peak: {info['used_memory_peak_human']}")

# Fragmentación > 1.5 indica mucha fragmentación — considerar restart o jemalloc
# Fragmentación < 1.0 indica que Redis está usando swap — alerta crítica

Técnicas de optimización:

  1. 1Usar tipos de datos compactos para objetos pequeños: los Hashes con menos de hash-max-listpack-entries campos usan ziplist (mucho más compacto)
  1. 2Comprimir valores grandes:
python
import zlib
import json

def set_compressed(key: str, data: dict, ttl: int = 3600):
    serialized = json.dumps(data).encode()
    if len(serialized) > 1024:  # comprimir si > 1KB
        compressed = zlib.compress(serialized, level=6)
        r.setex(f'c:{key}', ttl, compressed)
    else:
        r.setex(key, ttl, serialized)

def get_compressed(key: str) -> dict | None:
    # Intentar key comprimida primero
    data = r.get(f'c:{key}') or r.get(key)
    if not data:
        return None
    try:
        return json.loads(zlib.decompress(data))
    except zlib.error:
        return json.loads(data)
  1. 3TTL en todo: nunca guardes data sin TTL en un cache Redis
  2. 4Evitar KEYS \* en producción: usa SCAN con cursor

25. ¿Qué políticas de evicción tiene Redis y cuándo usás cada una?

Por qué la hacen: Si Redis se llena y no tiene política de evicción, empieza a rechazar escrituras.

Respuesta:

bash
# En redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru

| Política | Comportamiento |

|---|---|

| noeviction | Rechaza escrituras cuando está lleno. Para stores primarios. |

| allkeys-lru | Evicta las keys menos usadas recientemente. Default para caches. |

| volatile-lru | LRU sólo en keys con TTL. Para mezcla caché + persistente. |

| allkeys-lfu | Evicta las keys usadas con menos frecuencia (Redis 4+). Mejor para patrones de acceso desiguales. |

| volatile-ttl | Evicta las keys que expiran más pronto. |

| allkeys-random | Evicción aleatoria. Raramente útil. |

Regla práctica:

  • Redis como caché puro: allkeys-lru o allkeys-lfu
  • Redis como store + caché mixto: volatile-lru (sólo evicta los que tienen TTL)
  • Redis como store primario: noeviction con alertas de memoria

26. ¿Qué es el encoding interno de Redis y por qué importa?

Por qué la hacen: Preguntas de nivel senior sobre eficiencia de memoria.

Respuesta: Redis usa diferentes encodings internos según el tamaño de la estructura:

bash
# Ver el encoding interno de una key
redis-cli OBJECT ENCODING user:1001
# Puede retornar: ziplist, listpack, hashtable, skiplist, intset, embstr, raw, etc.

| Tipo | Encoding pequeño | Encoding grande |

|---|---|---|

| Hash | listpack (≤128 entries, ≤64 bytes/value) | hashtable |

| List | listpack (≤128 entries) | quicklist |

| Set (solo enteros) | intset | hashtable |

| Sorted Set | listpack (≤128 entries) | skiplist + hashtable |

| String (entero) | int | embstr/raw |

bash
# Configurar los umbrales en redis.conf
hash-max-listpack-entries 128
hash-max-listpack-value 64
zset-max-listpack-entries 128
zset-max-listpack-value 64
set-max-intset-entries 512

Impacto práctico: un Hash con 50 campos en listpack puede usar 5-10x menos memoria que en hashtable. Si tus objetos están cerca del umbral, vale la pena medir con MEMORY USAGE.


27. ¿Cómo evitás las operaciones O(N) que pueden bloquear Redis?

Por qué la hacen: Redis es single-threaded para comandos. Un KEYS * en producción puede pausar todo el servidor por segundos.

Respuesta:

python
# MAL: KEYS bloquea el servidor mientras escanea TODAS las keys
all_keys = r.keys('user:*')  # puede ser millones de keys

# BIEN: SCAN es incremental y no bloquea
def scan_keys(pattern: str, count: int = 100):
    cursor = 0
    while True:
        cursor, keys = r.scan(cursor, match=pattern, count=count)
        for key in keys:
            yield key
        if cursor == 0:
            break

for key in scan_keys('user:*'):
    process_key(key)

# Del mismo modo: SSCAN, HSCAN, ZSCAN para estructuras grandes
cursor = 0
while True:
    cursor, members = r.sscan('large:set', cursor, count=100)
    for member in members:
        process(member)
    if cursor == 0:
        break

Comandos O(N) a vigilar:

  • KEYS → usar SCAN
  • SMEMBERS en sets grandes → usar SSCAN
  • LRANGE key 0 -1 en listas largas → paginar con rangos acotados
  • HGETALL en hashes enormes → usar HSCAN
  • SORT sin LIMIT → siempre usar LIMIT offset count

Preguntas de system design con Redis

28. ¿Cómo diseñarías un sistema de sesiones con Redis?

Por qué la hacen: Caso de uso real y frecuente en cualquier app web.

Respuesta:

python
import secrets
import json
from datetime import timedelta

SESSION_TTL = int(timedelta(days=7).total_seconds())

def create_session(user_id: str, metadata: dict = None) -> str:
    session_id = secrets.token_urlsafe(32)
    session_data = {
        'user_id': user_id,
        'created_at': time.time(),
        'last_active': time.time(),
        **(metadata or {})
    }
    r.setex(f'session:{session_id}', SESSION_TTL, json.dumps(session_data))
    # Mantener lista de sesiones por usuario (para invalidación total)
    r.sadd(f'user_sessions:{user_id}', session_id)
    r.expire(f'user_sessions:{user_id}', SESSION_TTL)
    return session_id

def get_session(session_id: str) -> dict | None:
    raw = r.get(f'session:{session_id}')
    if not raw:
        return None
    session = json.loads(raw)
    # Sliding expiration: resetear el TTL en cada acceso
    r.expire(f'session:{session_id}', SESSION_TTL)
    session['last_active'] = time.time()
    r.setex(f'session:{session_id}', SESSION_TTL, json.dumps(session))
    return session

def invalidate_all_sessions(user_id: str):
    """Cerrar todas las sesiones del usuario (ej: cambio de contraseña)"""
    session_ids = r.smembers(f'user_sessions:{user_id}')
    if session_ids:
        pipe = r.pipeline()
        for sid in session_ids:
            pipe.delete(f'session:{sid}')
        pipe.delete(f'user_sessions:{user_id}')
        pipe.execute()

29. ¿Cómo implementarías un leaderboard en tiempo real con millones de usuarios?

Por qué la hacen: Sorted Set es la respuesta obvia, pero hay que pensar en la escala.

Respuesta:

python
# Leaderboard global con Sorted Set
def update_score(user_id: str, delta: int):
    new_score = r.zincrby('leaderboard:global', delta, user_id)
    return new_score

def get_rank(user_id: str) -> int:
    rank = r.zrevrank('leaderboard:global', user_id)
    return rank + 1 if rank is not None else None

def get_top_n(n: int = 10) -> list:
    return r.zrevrange('leaderboard:global', 0, n - 1, withscores=True)

def get_around_user(user_id: str, range_size: int = 5) -> list:
    """Top 5 por encima y 5 por debajo del usuario"""
    rank = r.zrevrank('leaderboard:global', user_id)
    if rank is None:
        return []
    start = max(0, rank - range_size)
    end = rank + range_size
    return r.zrevrange('leaderboard:global', start, end, withscores=True)

# Para escala masiva: leaderboards particionados por período
def get_current_week_key() -> str:
    week = time.strftime('%Y-W%W')
    return f'leaderboard:weekly:{week}'

def update_weekly_score(user_id: str, delta: int):
    key = get_current_week_key()
    r.zincrby(key, delta, user_id)
    r.expire(key, 60 * 60 * 24 * 14)  # 2 semanas de retención

Para millones de usuarios: un Sorted Set con 10M members usa ~600MB de RAM. Estrategias:

  • Particionar por región geográfica: leaderboard:global:AR, leaderboard:global:MX
  • Leaderboard de amigos: ZUNIONSTORE con los sets de amigos del usuario
  • Cálculo offline de rankings absolutos, sólo los top N en Redis

30. ¿Cómo usarías Redis para implementar un sistema de autocomplete?

Por qué la hacen: Sorted Set tiene un truco elegante para autocomplete.

Respuesta:

python
# Técnica: usar scores 0 para todos y ordenar lexicográficamente
# ZRANGEBYLEX funciona sobre el member cuando todos tienen el mismo score

def add_autocomplete_term(term: str, namespace: str = 'search'):
    # Agregar el término y todos sus prefijos
    r.zadd(f'autocomplete:{namespace}', {term: 0})

def autocomplete(prefix: str, namespace: str = 'search', limit: int = 10) -> list:
    return r.zrangebylex(
        f'autocomplete:{namespace}',
        f'[{prefix}',           # desde el prefijo (inclusive)
        f'[{prefix}\xff',       # hasta el prefijo + \xff (máximo carácter)
        start=0,
        num=limit
    )

# Poblar
for term in ['python', 'pytorch', 'pandas', 'postgresql', 'pubsub']:
    add_autocomplete_term(term)

print(autocomplete('py'))  # ['pandas', 'postgresql', 'pubsub', 'python', 'pytorch']
print(autocomplete('pyt')) # ['python', 'pytorch']

Para autocomplete con popularidad (los más buscados primero):

python
def add_term_with_popularity(term: str, score: float):
    r.zadd('autocomplete:popular', {term: score})

def popular_autocomplete(prefix: str, limit: int = 10) -> list:
    # Necesita un approach diferente: buscar por score descendente
    # filtrado por prefijo en aplicación
    candidates = r.zrevrange('autocomplete:popular', 0, 1000, withscores=True)
    return [
        (term, score) for term, score in candidates
        if term.startswith(prefix)
    ][:limit]

31. ¿Cómo implementarías un sistema de jobs queue con Redis?

Por qué la hacen: Muy común en backends modernos. Librerías como Celery, BullMQ usan Redis por debajo.

Respuesta:

python
import json
import uuid
import time

class SimpleJobQueue:
    def __init__(self, redis_client, queue_name: str):
        self.r = redis_client
        self.queue = f'jobs:{queue_name}'
        self.processing = f'jobs:{queue_name}:processing'
        self.failed = f'jobs:{queue_name}:failed'
    
    def enqueue(self, payload: dict, delay: float = 0) -> str:
        job_id = str(uuid.uuid4())
        job = {
            'id': job_id,
            'payload': payload,
            'enqueued_at': time.time(),
            'attempts': 0
        }
        
        if delay > 0:
            # Cola retrasada con Sorted Set (score = timestamp de ejecución)
            execute_at = time.time() + delay
            self.r.zadd(f'{self.queue}:delayed', {json.dumps(job): execute_at})
        else:
            self.r.rpush(self.queue, json.dumps(job))
        
        return job_id
    
    def dequeue(self, timeout: int = 30) -> dict | None:
        # Primero mover delayed jobs que ya deben ejecutarse
        self._move_delayed_jobs()
        
        # BRPOPLPUSH: atómico, mueve el job a la lista de processing
        raw = self.r.blmove(self.queue, self.processing, timeout=1, src='LEFT', dest='RIGHT')
        if raw:
            return json.loads(raw)
        return None
    
    def _move_delayed_jobs(self):
        now = time.time()
        jobs = self.r.zrangebyscore(f'{self.queue}:delayed', 0, now)
        if jobs:
            pipe = self.r.pipeline()
            for job in jobs:
                pipe.rpush(self.queue, job)
                pipe.zrem(f'{self.queue}:delayed', job)
            pipe.execute()
    
    def ack(self, job: dict):
        self.r.lrem(self.processing, 1, json.dumps(job))
    
    def nack(self, job: dict, retry: bool = True):
        job['attempts'] += 1
        self.r.lrem(self.processing, 1, json.dumps(job))
        
        if retry and job['attempts'] < 3:
            delay = 60 * (2 ** job['attempts'])  # backoff exponencial
            self.r.zadd(f'{self.queue}:delayed', {json.dumps(job): time.time() + delay})
        else:
            self.r.rpush(self.failed, json.dumps(job))

32. ¿Cómo detectarías y prevenirías hot keys en Redis?

Por qué la hacen: Hot keys pueden saturar un nodo en Redis Cluster y crear cuellos de botella.

Respuesta: Una hot key es una key que recibe una fracción desproporcionada del tráfico. En Redis Cluster, esto satura el nodo que tiene ese slot, sin importar cuántos otros nodos haya.

bash
# Detectar hot keys (requiere Redis 4+ con LFU)
redis-cli --hotkeys -h node-1 -p 7000

# Monitorear comandos en tiempo real
redis-cli MONITOR | head -100

# Ver estadísticas de frecuencia
redis-cli OBJECT FREQ mykey  # sólo con maxmemory-policy lfu

Mitigaciones:

  1. 1Local cache en la app: cachear en memoria de la aplicación las keys más accedidas
python
from functools import lru_cache
from datetime import datetime, timedelta

_local_cache = {}
_local_cache_expiry = {}

def get_hot_config(key: str) -> str:
    """Configuración global accedida miles de veces por segundo"""
    now = datetime.now()
    if key in _local_cache and _local_cache_expiry[key] > now:
        return _local_cache[key]
    
    value = r.get(key)
    _local_cache[key] = value
    _local_cache_expiry[key] = now + timedelta(seconds=5)
    return value
  1. 2Key sharding: duplicar la key en múltiples slots
python
import random

def get_sharded(base_key: str, shards: int = 10) -> str:
    shard = random.randint(0, shards - 1)
    return r.get(f'{base_key}:shard:{shard}')

def set_sharded(base_key: str, value: str, shards: int = 10, ttl: int = 3600):
    pipe = r.pipeline()
    for i in range(shards):
        pipe.setex(f'{base_key}:shard:{i}', ttl, value)
    pipe.execute()

33. ¿Cómo usarías Redis para implementar un contador de visitas único por día?

Por qué la hacen: Evalúan el conocimiento de Bitmap y HyperLogLog.

Respuesta:

python
from datetime import date

def track_daily_unique(user_id: int, page: str):
    today = date.today().isoformat()
    key = f'unique:{page}:{today}'
    r.setbit(key, user_id, 1)          # O(1), usa 1 bit por user_id
    r.expire(key, 86400 * 7)           # guardar 7 días

def get_daily_unique_count(page: str, day: str = None) -> int:
    day = day or date.today().isoformat()
    return r.bitcount(f'unique:{page}:{day}')

def was_user_active_today(user_id: int, page: str) -> bool:
    today = date.today().isoformat()
    return bool(r.getbit(f'unique:{page}:{today}', user_id))

# Usuarios activos en los últimos 3 días (BITOP AND)
def active_last_3_days(page: str) -> int:
    keys = [f'unique:{page}:{date.today().isoformat()}']  # simplificado
    r.bitop('AND', 'temp:active3days', *keys)
    count = r.bitcount('temp:active3days')
    r.delete('temp:active3days')
    return count

# Para user_ids muy grandes o cuando no controlás el ID, usá HyperLogLog
def track_hll(ip: str, page: str):
    today = date.today().isoformat()
    r.pfadd(f'hll:{page}:{today}', ip)

def estimate_unique_ips(page: str) -> int:
    today = date.today().isoformat()
    return r.pfcount(f'hll:{page}:{today}')
    # Error de ~0.81%, usa máximo 12KB por clave

34. ¿Cómo implementarías un sistema de notificaciones push en tiempo real con Redis?

Por qué la hacen: Combina varias primitivas de Redis en un diseño end-to-end.

Respuesta:

python
# Arquitectura: Redis Pub/Sub para delivery en tiempo real + List para offline
# El cliente se conecta vía WebSocket, el servidor escucha los canales del usuario

class NotificationService:
    def __init__(self, redis_client):
        self.r = redis_client
    
    def send_notification(self, user_id: str, notification: dict):
        notification_id = str(uuid.uuid4())
        notification.update({'id': notification_id, 'ts': time.time(), 'read': False})
        
        # 1. Persistir en lista (para usuarios offline)
        key = f'notifications:{user_id}'
        self.r.lpush(key, json.dumps(notification))
        self.r.ltrim(key, 0, 99)  # Mantener sólo las últimas 100
        
        # 2. Publicar para usuarios online
        self.r.publish(f'live:user:{user_id}', json.dumps(notification))
        
        # 3. Incrementar badge counter
        self.r.incr(f'notifications:unread:{user_id}')
        
        return notification_id
    
    def get_notifications(self, user_id: str, limit: int = 20) -> list:
        key = f'notifications:{user_id}'
        items = self.r.lrange(key, 0, limit - 1)
        return [json.loads(item) for item in items]
    
    def mark_all_read(self, user_id: str):
        self.r.set(f'notifications:unread:{user_id}', 0)
        notifications = self.get_notifications(user_id, 100)
        pipe = self.r.pipeline()
        key = f'notifications:{user_id}'
        pipe.delete(key)
        for notif in notifications:
            notif['read'] = True
            pipe.rpush(key, json.dumps(notif))
        pipe.execute()
    
    def get_unread_count(self, user_id: str) -> int:
        count = self.r.get(f'notifications:unread:{user_id}')
        return int(count or 0)

35. ¿Cómo debugueás performance issues en Redis en producción?

Por qué la hacen: Cualquier ing. senior necesita saber operar Redis, no sólo usarlo.

Respuesta:

bash
# 1. SLOWLOG: ver los comandos más lentos
redis-cli SLOWLOG GET 10
redis-cli SLOWLOG LEN
redis-cli CONFIG SET slowlog-log-slower-than 1000  # en microsegundos

# 2. INFO stats para ver patrones generales
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses|rejected_connections'

# 3. LATENCY: historial de latencia
redis-cli LATENCY HISTORY event
redis-cli LATENCY LATEST
redis-cli LATENCY RESET

# 4. CLIENT LIST: ver conexiones activas y sus estados
redis-cli CLIENT LIST

# 5. MONITOR: ver todos los comandos en tiempo real (NO usar en producción bajo carga)
redis-cli MONITOR | grep -E 'KEYS|HGETALL' | head -20
python
# Desde código: instrumentar todas las operaciones
import time
from contextlib import contextmanager

@contextmanager
def redis_timer(operation: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = (time.perf_counter() - start) * 1000
        if elapsed > 10:  # alertar si > 10ms
            print(f"SLOW REDIS OP: {operation} took {elapsed:.2f}ms")

with redis_timer('get_user_1001'):
    user = r.get('user:1001')

# Verificar hit rate del cache
info = r.info('stats')
hits = info['keyspace_hits']
misses = info['keyspace_misses']
hit_rate = hits / (hits + misses) if (hits + misses) > 0 else 0
print(f"Cache hit rate: {hit_rate:.1%}")

Checklist de performance:

  1. 1Hit rate < 80% → revisar TTLs y estrategia de caché
  2. 2rejected_connections > 0 → aumentar maxclients o agregar instancias
  3. 3blocked_clients altos → muchos BLPOP/BRPOP, revisar si es esperado
  4. 4mem_fragmentation_ratio > 1.5 → considerar MEMORY PURGE o restart
  5. 5Latencia alta en operaciones → buscar con SLOWLOG, posiblemente hay O(N) operations

Lo que busca el entrevistador: resumen

En preguntas de estructuras de datos: esperan que elijas la estructura correcta por los trade-offs, no por costumbre. "Usé un Hash porque..." es mejor que "usé un Hash siempre".

En preguntas de persistencia: quieren ver que entendés que Redis puede perder datos y que elegís conscientemente la política según los requerimientos del negocio.

En preguntas de clustering y HA: buscan que distingas entre escalabilidad horizontal (Cluster) y alta disponibilidad (Sentinel), y que sepas que no son lo mismo.

En preguntas de locks y transacciones: el detalle que diferencia al mid del senior es saber por qué el check-then-act no atómico es una race condition y cómo Lua o WATCH lo resuelven.

En preguntas de system design: no hay respuesta única. Tienen que ver cómo pensás los trade-offs: latencia vs durabilidad, memoria vs features, complejidad vs resiliencia.


Errores comunes en entrevistas

  1. 1"Redis es solo un caché" — es una base de datos multi-modelo. Streams, Sorted Sets y Hashes tienen casos de uso que van mucho más allá del caché.
  1. 2No mencionar TTL — olvidarte de EXPIRE en un diseño es una señal de alerta para el entrevistador.
  1. 3Usar KEYS \* en producción — si lo mencionás sin aclarar que es sólo para desarrollo, perdés puntos.
  1. 4Confundir pipeline con transacción — pipeline es optimización de red, no atomicidad.
  1. 5No conocer los límites — "¿qué pasa cuando se llena Redis?" es una pregunta de seguimiento frecuente. Tené claro maxmemory-policy.
  1. 6Asumir que Redis Cluster resuelve todo — un Cluster con hot keys sigue siendo un problema. Hay que diseñar la distribución de keys activamente.

FAQ

¿Cuáles son las preguntas de Redis más frecuentes en entrevistas?+

Las más frecuentes son: diferencia entre RDB y AOF, implementación de rate limiting, locks distribuidos con SET NX EX, cache stampede y cómo prevenirlo, diferencia entre Pub/Sub y Streams, y cómo funciona Redis Cluster con hash slots. También son comunes las preguntas de Sorted Set para leaderboards y de optimización de memoria.

¿Necesito saber Redis para entrevistas de backend senior?+

Sí. Redis aparece en prácticamente toda entrevista de backend mid-senior y en entrevistas de system design. Es esperado que sepas usarlo para caché, sesiones, rate limiting, queues y locks distribuidos. Conocer los trade-offs de persistencia y los patrones de invalidación de caché diferencia a los candidatos.

¿Cuál es la diferencia entre Redis Cluster y Redis Sentinel?+

Sentinel provee alta disponibilidad para una sola instancia: monitorea el master, detecta fallos y promueve una réplica automáticamente. Cluster provee sharding horizontal: distribuye los datos en múltiples nodos usando hash slots (16384 en total). Podés combinar ambos — un Cluster donde cada nodo master tiene réplicas gestionadas por Sentinel.

¿Cómo implemento rate limiting con Redis?+

Hay tres enfoques principales: Fixed Window Counter (INCR + EXPIRE por ventana de tiempo, simple pero con burst boundary problem), Sliding Window Log (Sorted Set con timestamps, preciso pero usa más memoria), y Token Bucket con scripts Lua (el más flexible, maneja bursts naturalmente). Para producción se recomienda el Token Bucket o Sliding Window dependiendo de si necesitás permitir bursts.

¿Cuándo uso Lua en Redis en lugar de MULTI/EXEC?+

Usás Lua cuando necesitás lógica condicional atómica: leer un valor, evaluarlo, y escribir según el resultado, todo en una operación indivisible. MULTI/EXEC no permite lógica condicional basada en valores leídos dentro de la transacción. Los scripts Lua en Redis se ejecutan atómicamente en el servidor, sin posibilidad de que otro cliente intercale operaciones.

¿Redis puede reemplazar a Kafka?+

Para casos de uso livianos, sí. Redis Streams provee un log persistente con grupos de consumidores y ACK, similar a Kafka. La diferencia clave es la escala: Redis Streams está limitado por la RAM disponible y el throughput de una instancia (~100k msg/s), mientras que Kafka puede manejar millones de mensajes por segundo con retención a largo plazo en disco. Usá Redis Streams para eventos internos de baja escala; usá Kafka para event sourcing y analytics a escala empresarial.

Artículos relacionados

Preguntas de entrevista de diseño de sistemas distribuidos (35+)

Complete system design interview article in Spanish rioplatense with 40 questions, detailed answers, real code

Preguntas de entrevista de GraphQL — 35 con código y respuestas

35 preguntas de GraphQL para entrevistas backend y full-stack: queries, mutations, subscriptions, resolvers, DataLoader, N+1 problem, schema design. Con código.

Preguntas de entrevista de Microservicios — 35 respuestas de arquitecto

35 preguntas de microservicios para roles backend y arquitecto: descomposición de servicios, saga pattern, event sourcing, circuit breaker, observabilidad. Respuestas de nivel senior.

System Design: cómo diseñar un sistema de pagos en una entrevista

Guía paso a paso para diseñar un sistema de pagos en una entrevista de system design: idempotencia, consistencia, colas de transacciones, reconciliación, seguridad PCI DSS.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY