R8.03 · Production & Operations — module hub
Pinterest: task queues & partitioning.
PinLater is a Thrift service Pinterest built to schedule and run asynchronous jobs — enqueue, dequeue, ACK over two storage backends, more than 500 job queues processing north of six million jobs per minute. It proves two patterns worth taking apart: functional partitioning, and the list-based reliable queue.
Grounding: the Pinterest source pack — Open-sourcing PinLater and
Sharding Pinterest (Pinterest Engineering), the pinterest/pinlater README, and the
Redis RPOPLPUSH/LMOVE docs — for the case study; the as-built
echo/apps/echo_mq and echo/apps/echo_data for the applied half. The bus is
Valkey-only; Pinterest’s own Redis/MySQL/memcache is the case study, quoted as theirs.
Static facts — partitioning → the id tells you the shard · free shard → adding a shard is config, not migration · reliable → no job lost between claim and ack · durability → a job store that must not lose work wants a durable record.
The system: PinLater
PinLater · pinterest/pinlater“PinLater is a Thrift service to manage scheduling and execution of asynchronous jobs.” Written in Java, Apache-2.0, open-sourced in 2014 and archived in February 2018 (succeeded by Pacer). Its surface is three core actions — enqueue, dequeue, ACK — over three components: a stateless Thrift service for job submission and scheduling, a storage backend that holds the jobs and their state, and worker pools that execute the jobs.
Reliability is explicit acks with automatic retry at a configurable delay: a worker replies a positive or negative ACK depending on whether execution succeeded or failed, and a failed job is retried later. Per-job knobs are set at enqueue time — “Priorities, delayed execution and flexible retry policies that can be specified per job at enqueue time” — plus checkpointing for long-running jobs.
Two storage backends sit behind it. The README guidance: services should “default to use the MySQL backend as long as the QPS is in the lower to mid range (no more than 1000 QPS per shard). If the QPS is expected to be higher than this, then the Redis implementation should be used.” Redis was the high-throughput backend; MySQL was the durable default.
PinLater is the lens. The reader is building a job system too — EchoMQ, backed by Valkey — so each idea below lands twice: the Pinterest fact, and the move it becomes on the bus.
The two ideas it proves
Sharding Pinterest · Redis docsFunctional partitioning — polyglot by access pattern. Pinterest keeps different
data on different stores, chosen by how it is read: “We keep pin_id → pin object
cache in a memcache cluster, but we keep board_id → pin_ids in a redis cluster.”
Different data, different store — not forced into one engine. And the id itself carries placement: the
sharded fleet packs the shard into the high bits of a 64-bit id,
ID = (shard ID << 46) | (type ID << 36) | (local ID<<0) (shard id 16 bits,
type id 10 bits, local id 36 bits), so the id tells you where the row lives — no lookup table.
The list-based reliable queue. A consumer atomically moves a job off the work list onto
a processing list as it claims it (RPOPLPUSH source processing, blocking
BRPOPLPUSH), then LREM removes it from the processing list once the job is
acknowledged. If the worker crashes mid-job, the job is still on the processing list — a recovery
worker scans for items stuck too long and re-queues them. No job is lost to a crash between receive and ack.
RPOPLPUSH/BRPOPLPUSH were deprecated in Redis 6.2.0 in favour of
LMOVE/BLMOVE (which take an explicit LEFT/RIGHT
direction); LMOVE is today’s primitive, RPOPLPUSH the original.
# the list-based reliable queue — the modern primitive (Redis 6.2.0+): BLMOVE work processing RIGHT LEFT 5 # claim: atomically move one job to the processing list, block up to 5 s # … run the job … LREM processing 1 <job> # ack: remove it from the processing list # a crash before LREM leaves the job on `processing`; a recovery worker re-queues it
Two patterns, one system: put a queue’s data where its access pattern wants it and let the id carry placement; then move a claimed job somewhere safe so a crash never loses it.
Scale, and the durability turn
Sharding Pinterest · PinLaterThe fleet behind PinLater grew through a wall. Pinterest started from “eight EC2 servers
running one MySQL instance each”; by September 2011 “every piece of our infrastructure
was over capacity.” They finished launching the sharding approach “in early 2012, and
it’s still the system we use today,” carrying 50 billion Pins across
one billion boards. PinLater’s own sharding is a free sharding scheme
— a shard is chosen when a job is enqueued — so adding a shard is a configuration
change, not a data migration, and load spreads evenly. The shard id rides in the job descriptor itself,
[queue_name][shard_id][priority][local_id].
The case study’s punch line is the durability turn. Pinterest later got “5X more throughput on the MySQL backend, which allows us to do over 2,000 enqueues per second with a single i2.2xl MySQL EC2 instance” and so chose to “move all our workloads to MySQL and deprecate the use of the Redis backend” — for MySQL’s durability and replication. A list-in-RAM queue is fast, but a job store that must not lose work eventually wants a durable record. That is the same tension R7.01 (Redis as a primary database) and the persistence floor name.
The applied half — the BCS bus
echo_mq/keyspace.ex · jobs.ex · branded_id.exThe Pinterest case is the lens; the bus the reader is building is EchoMQ, backed by Valkey. Each idea
ties back to one verified surface. Partitioning is the braced keyspace: per-queue keys are
emq:{q}:<type> and every key of one queue lands on one slot, computed client-side as
slot/1 = rem(crc16(hashtag(key), 0), 16384) — CRC16-XMODEM over the {…}
substring, mod 16384, the cluster-spec algorithm — with the on-disk vector
slot(“123456789”) == 12739. Crash-safety is a server-clock lease plus an
attempts counter, not a processing list: the claim stamps
redis.call('TIME') and the job HASH carries state / attempts /
payload (jobs.ex:
HSET KEYS[1] 'state' 'pending' 'attempts' '0' 'payload' …). A crash lets the lease
expire; the job is re-claimed and attempts increments.
Functional partitioning puts a queue’s data on a known shard and lets the identity decide placement; the list-based reliable queue moves a claimed job somewhere safe so a crash between claim and ack never loses it.
EchoMQ.Keyspace pins a queue’s keys to one slot via the {q} hashtag;
EchoMQ.Jobs claims with a server clock and a job HASH (state/attempts/payload),
so a crash re-claims the job and bumps attempts — the modern lease-based equivalent of
the processing list, reaching the same goal: no lost job, bounded retries.
Two honest framings keep the parallel straight:
- The lane ring is not a processing list.
EchoMQ.Lanesdoes runredis.call('LMOVE', KEYS[1], KEYS[1], 'LEFT', 'RIGHT'), but that is a single-key ring rotation — it rotates the list of lane names round-robin so service spreads across lanes; it is not asource → processingmove. The owned wire still knows the family —EchoWire.Cmd.rpoplpush/2(“Open an RPOPLPUSH builder”) andblmoveexist inecho_wire/cmd.ex. PinLater and classic-Redis recover via a processing list and a recovery worker; EchoMQ reaches the same goal through the lease. - Placement is computed from identity, not looked up. Pinterest embeds the
shard in the id’s high bits (
shard ID << 46, a bit-extract). The branded id is hashed to a placement — the contract vectorEchoData.BrandedId.hash32(274557032793636864) == 234878118(the idUSR0KHTOWnGLuC) — and a queue’s co-location comes from the{q}hashtag, not from the snowflake’s minting-node field. The shared principle is the one to keep: placement is a property of the identity, computed rather than stored in a registry — Pinterest by bit-extraction, echo by hash plus the hashtag.
The worked consumer is codemojex. Codemojex.Guesses.submit/3 mints a
branded JOB (EchoData.BrandedId.generate!(“JOB”)) and runs
Lanes.enqueue(Bus.conn(), “cm”, player, job, payload) — a guess is a job on
the player’s lane, so the bus rotates service across players and one keyboard masher cannot starve
the field.
/echomq
The EchoMQ protocol behind the reliable-queue and partitioning tie-backs — the owned wire, the braced keyspace, the Lua layer.
door · the Queue pillar/echomq/queue
Jobs, lanes, and the server-clock lease in depth — the claim, the attempts fence,
and the per-lane fair rotation.
/echo-persistence
Where the job record is made durable — the durability dial PinLater answered by moving from the Redis backend to MySQL.
Notes on ValkeyA queue’s keys co-locate because the
{q} hashtag forces the slot: a key is hashed only over the substring between the first
{ and }, and the slot is CRC16(hashtag) mod 16384. Every
emq:{orders}:* key lands on one slot, so a multi-key Lua script over one queue is always a
single-slot operation —
valkey.io/topics/cluster-spec.
The three dives
partition → reliable → scaleThe module takes the two ideas apart and then follows the scale arc: partition first, then make the queue reliable, then scale it out. Read them in order.
Functional partitioning
The polyglot split and the shard-in-the-id scheme; the BCS echo — the
{q} hashtag and CRC16 slot, the branded id hashed to a placement.
List-based reliable queues
LMOVE onto a processing list, LREM on ack, the recovery worker;
the BCS echo — a server-clock lease plus attempts.
1 → 1000+ scaling
Eight EC2 servers to the early-2012 sharded fleet; free sharding so a shard is config not migration; the Redis-to-MySQL durability turn.
Three dives, one async-job system: pin a queue’s keys by identity, never lose a job to a crash, and make “add a shard” trivial — the Pinterest case study and the BCS bus read side by side.
After R8.03, the chapter continues to R8.04 — Twitter/X: internals and custom structures (specified) — before the capstone hands off to the dedicated EchoMQ course. Return to the Production & Operations chapter to continue, or open the Queue pillar for the depth behind the lease and the lanes.
References
sources · relatedSources
- Pinterest Engineering — Open-sourcing PinLater — the Thrift async-job service: enqueue/dequeue/ACK, two backends, explicit acks with retry, 500+ queues and 6M+ jobs per minute.
- GitHub — pinterest/pinlater — the README: the job descriptor format, the free sharding scheme, the MySQL-vs-Redis backend guidance.
- Pinterest Engineering — Sharding Pinterest — the growth wall, the shard-in-the-id scheme, functional partitioning, and the move-cost principle.
- Redis — LMOVE — the modern reliable-queue primitive (
RPOPLPUSHdeprecated in 6.2.0). - Redis — RPOPLPUSH — the original reliable-queue pattern and the processing-list recovery note.
- Valkey — Cluster specification — CRC16 mod 16384 hash slots and the
{…}hashtag that forces a queue’s keys onto one slot.
Related in this course
- R8.03.1 · Functional partitioningthe id carries placement
- R8.03.2 · List-based reliable queuesnever lose a job to a crash
- R8.03.3 · 1 → 1000+ scalingfree sharding, the durability turn
- R8 · Production & Operationsthe chapter
- /echomqthe EchoMQ protocol behind the tie-backs
- /echomq · Queuejobs, lanes, leases