A production-grade design that goes well beyond a basic queue — covering priority delivery, consumer fault tolerance, TTL-based expiry, hybrid push/poll, and Redis-native rate limiting.
Problem Statement
Continuous polling hammers the server even when there are no messages — wasting CPU and database connections.
Naive queues treat a system alert and an analytics event identically. Critical messages can sit behind thousands of low-priority ones.
If a consumer reads a message and then crashes, the message is silently lost with no way to re-deliver or investigate.
Without rate limiting, a single misbehaving producer can exhaust queue capacity, starving every other consumer.
High-Level Architecture
Producer (any HTTP client)
│
▼
REST API ─── Spring Boot ─── Rate Limiter (Redis INCR+EXPIRE)
│
▼
Redis ─────────────────────────────────────────────────────
│ Sorted Set → priority queue (messages:{channel}) │
│ Pub/Sub → real-time push (channel:*) │
│ List → retry buffer (retry:{channel}) │
│ List → dead-letter (dlq:{channel}) │
│ String → rate counters (rl:{key}:{window}) │
│
▼
Consumer Service
├─ Supports real-time? → Redis Pub/Sub push
└─ Polling fallback? → GET /v1/messages/poll (long-poll)
│
▼
ACK → message removed from processing queue
No ACK within timeout → re-queue (max N retries → DLQ)Redis Data Structures
| Data Structure | Use in BlinkQ |
|---|---|
| Sorted Sets | Priority queue — score = priority × 1e9 + unix_ms |
| Pub/Sub | Real-time push to consumers that support it |
| Strings / INCR | Sliding-window rate limiter per producer key |
| Key Expiry (TTL) | Automatic stale-message cleanup — no cron needed |
| Lists | Processing queue, retry buffer, and dead-letter queue |
Core Innovations
Consumers that open a WebSocket or SSE channel receive messages via Redis Pub/Sub with sub-millisecond latency. Consumers that can only poll use long-polling via GET /messages/poll — the server blocks until a message is available or the timeout expires. The switch is transparent to the producer.
Each message is inserted into a Redis Sorted Set with score = priority × 10⁹ + unix_ms. ZPOPMAX atomically retrieves the highest-priority, earliest message. Priority 0 = analytics noise; Priority 10 = production incident. No separate queues per priority needed.
Every message carries a TTL field. On publish, Redis EXPIREAT is set on the message key. Stale messages are never delivered — Redis purges them automatically. This eliminates the need for a cleanup cron job and keeps queue memory bounded.
After delivering a message, BlinkQ adds it to a processing set with a delivery timestamp. The consumer must POST /messages/ack within a configurable window. If not, the message is moved back to the priority queue. After N failed retries it is appended to the dead-letter queue for manual inspection via GET /dead-letter.
Each producer API key has a sliding-window rate limit enforced with INCR + EXPIRE. The counter key is namespaced by key + second-aligned window. If the counter exceeds the configured threshold, the API returns HTTP 429. No external rate-limit store needed — Redis handles it atomically.
API Design
| Method | Path | Description |
|---|---|---|
| POST | /v1/messages | Publish a message with priority & TTL |
| POST | /v1/messages/bulk | Batch-publish up to 100 messages |
| GET | /v1/messages/poll | Long-poll until a prioritised message arrives |
| POST | /v1/messages/ack | Confirm delivery; clears message from processing queue |
| GET | /v1/metrics | Queue depth, throughput, error rates (admin) |
| GET | /v1/dead-letter | Inspect messages that exhausted all retries |
Technology Stack
REST API, business logic, ACK tracking
Message broker, rate limiter, TTL engine
Containerised dev & prod environment
Deployment target for Redis and services
Real-time push path for live consumers
Metrics: queue depth, latency, error rate
Sign up, generate an API key, and publish your first priority message in under two minutes.