jonnify·redis /redis-patterns/production-operations/pinterest-task-queue/scaling-1-to-1000

R8.03.3 · Pinterest: task queues & partitioning — dive 3

From one box to a fleet: 1 → 1000+ scaling.

Pinterest scaled from eight servers to a fleet carrying fifty billion Pins. The move that made it possible was not a bigger machine — it was making “add a shard” a configuration change, not a data migration.

Grounding: Pinterest engineering — Sharding Pinterest & Open-sourcing PinLater (the scale figures, the free-sharding scheme) · echo_mq/lib/echo_mq/keyspace.ex (the {q} hashtag → CRC16 slot) · echo_mq/lib/echo_mq/jobs.ex (the job HASH that is the record of truth).

Trace the growth — eight servers to a sharded fleet. Select a stage to read the real Pinterest figure and what made the next step reachable.
2011 2012 → 8 EC2 serversone MySQL each over capacitySept 2011 sharded fleet50B Pins / 1B boards
readout · Select a stage. The arc is one box to a fleet; the lesson is what made each step reachable — deciding placement up front so adding capacity never means moving the data already stored.

Real figures (Pinterest engineering) — the fleet started from eight EC2 servers running one MySQL instance each · by September 2011 every piece of the infrastructure was over capacity · the sharded approach launched in early 2012 and carried 50 billion Pins across one billion boards.

§1

Eight servers, then over capacity

Sharding Pinterest · the growth wall

Pinterest’s growth through 2011 was explosive. The fleet started small — eight EC2 servers running one MySQL instance each — and the engineering account is blunt about where that ended: by September 2011, “every piece of our infrastructure was over capacity.”

The answer they shipped was a sharded fleet. They finished launching the sharding approach “in early 2012, and it’s still the system we use today,” and it carried 50 billion Pins across one billion boards. The title of this dive is that arc — one box to a fleet — and the lesson is not the size of the fleet but what made it reachable: a scheme where adding capacity does not mean moving the data already stored.

Pinterest stated the principle that governed every capacity move: “We hated moving data around, especially item by item… If we had to move data, it was better to move an entire virtual node to a different physical node.” Capacity is added by upgrading a machine, opening a new shard range, or migrating a whole virtual node — never by re-distributing rows one at a time. A migration that touches every item does not scale; one that relocates a whole bucket does.

The arc is eight servers to a fleet of fifty billion Pins. The mechanism behind it is one rule: decide placement up front, and add capacity by widening the set — never by moving items one by one.

§2

PinLater’s free sharding

Open-sourcing PinLater · the descriptor

PinLater, Pinterest’s asynchronous job system, made “add a shard” trivial by putting the shard id where it could never need a lookup: inside the job’s own descriptor. The descriptor format, verbatim from the project’s documentation:

# the PinLater job descriptor — the shard id is carried in the job itself
[queue_name][shard_id][priority][local_id]

Shards live within a queue. PinLater uses a free sharding scheme — a shard is chosen when a job is enqueued — and the chosen shard is then carried in the descriptor for the life of the job. Two consequences follow directly.

config, not migration
The shard is selected at enqueue and recorded in the descriptor, so a new shard receives jobs as soon as it is listed in the configuration. No existing job moves; load starts spreading across the wider set.
the descriptor locates the job
A worker reading a descriptor knows the queue, the shard, the priority, and the local id without a registry. The id tells you where the job lives.

This is the same scaling posture at the queue layer that the sharded fleet gives at the database layer: the placement decision is made once, written into the identity, and never revisited. PinLater ran it at real scale — “more than 500 job queues processing north of six million jobs per minute” on “more than 10 different clusters all on Amazon EC2.”

Two ways to fix placement up front. Both record where data lives in the identity, so adding capacity never moves what is already stored. Select one to read its mechanism.
queue_name shard_id priority local_id PinLater · shard chosen at enqueue, carried in the descriptor emq: {queue} :type slot 12739 EchoMQ · slot = CRC16(hashtag) mod 16384, a function of the name
readout · Select a scheme. Both fix placement in the identity — PinLater in the descriptor at enqueue, EchoMQ in the slot computed from the queue name — so capacity is added by widening the set, not by moving data.

The shard lives in the descriptor, chosen once at enqueue. Adding a shard lists it in the config; no job already enqueued moves. That is what let 500+ queues and six million jobs a minute grow across more than ten clusters.

§3

The BCS echo — placement is a function of the name

echo_mq/lib/echo_mq/keyspace.ex · slot/1

The echo bus reaches the same “add a shard without moving data” property by a different mechanism, and the mechanism is the one the cluster specification defines. Every key of one queue is built with a hashtag, and the slot it lands on is computed client-side from that hashtag:

# the queue name "orders" sits inside the braces — the whole queue hashes together
emq:{orders}:pending

EchoMQ.Keyspace.slot/1 computes a key’s cluster slot as rem(crc16(hashtag(key), 0), 16384) — CRC16-XMODEM over the substring inside the first {...}, modulo 16384, exactly the cluster specification’s algorithm. The module’s own words: “every key of one queue lands on one slot.” A known vector pins the behaviour: slot("123456789") == 12739.

The scaling payoff is the same as PinLater’s free sharding. A queue’s slot is a pure function of the queue name — it is computed, never looked up, and it never changes. When the cluster grows from a few nodes to many, slots are reassigned to nodes, but a queue’s keys stay together on whatever node now owns their slot. No key is re-homed by name; the slot was decided the moment the queue was named. Pinterest decides placement at enqueue and writes it into the descriptor; the bus decides placement from the name and computes it on every call. Both make growth a matter of widening the set, not relocating the contents.

the application · the slot computed from the queue name

The function that fixes placement up front

The cluster slot is a pure function of the {q} hashtag — computed client-side, no registry, fixed for the life of the queue. Verbatim from the real keyspace module:

echo_mq/lib/echo_mq/keyspace.ex · slot/1frozen
# CRC16-XMODEM over the hashtag (inside the first {...}), modulo 16384 —
# the cluster specification's algorithm. Known vector: slot("123456789") == 12739
def slot(key) when is_binary(key), do: rem(crc16(hashtag(key), 0), 16384)

# every key of one queue lands on one slot — placement is the queue name,
# not a lookup, so the cluster can grow without re-homing a key.
def queue_key(queue, type), do: IO.iodata_to_binary(["emq:{", queue, "}:", type])

Notes on ValkeyThe braced hashtag and CRC16 modulo 16384 are the cluster specification, not an echo invention: keys whose {...} substring is identical hash to the same slot and so always live on one node — the engine’s own reference is valkey.io/topics/cluster-spec.

the pattern

Scale out by making placement a property fixed up front, so adding capacity widens the set without moving what is already stored. Pinterest chooses a shard at enqueue and records it in the [queue_name][shard_id][priority][local_id] descriptor.

its EchoMQ application

EchoMQ.Keyspace.slot/1 derives a queue’s slot from the {q} hashtag via CRC16 mod 16384, so a queue’s keys are co-located by name and the cluster can grow without re-homing them — placement computed, never looked up.

PinLater writes the shard into the descriptor; the bus computes the slot from the queue name. Two mechanisms, one property: placement is decided once, so the cluster grows by widening, not by moving.

§4

The durability turn

Open-sourcing PinLater · Redis → MySQL

The scale figures hide a second lesson, and PinLater states it as a turn. PinLater shipped with two storage backends, MySQL and Redis, and the guidance was throughput-driven: 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.

Then the trade reversed. Pinterest reported “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 of every job. This is the tension R7.01 (Redis as a primary database) names directly, and it is the reason the persistence floor exists.

The BCS stack answers the same tension not by replacing the engine but with a durability dial. The job is a HASH held in Valkey, and that HASH is the record of truth: the enqueue script writes HSET KEYS[1] 'state' 'pending' 'attempts' '0' 'payload' …, and state, attempts, and payload carry the job’s whole life. That record lives in RAM for speed, the way PinLater’s Redis backend did. Where it is made durable is the dial — ETS, then Valkey, then a committed engine behind it — the subject of /echo-persistence. PinLater moved a layer to gain durability; the BCS stack keeps one record of truth and turns the durability up underneath it.

the door · where the record of truth is made durable

The durability dial

PinLater hit the durability ceiling and moved Redis → MySQL. The BCS stack keeps the job HASH as the record of truth and dials durability up underneath it — the in-RAM layer, then Valkey, then a committed engine. The /echo-persistence chapter is that dial; the /echomq/queue pillar is the job HASH, the lanes, and the keyspace behind the slot.

Scaling out is making placement a decision you make once. Pinterest writes the shard into the descriptor; the bus computes the slot from the queue name. And scaling up against the durability ceiling is a dial, not a rewrite — the job HASH is the record of truth in RAM, and /echo-persistence is where it is made durable.

§5

Recap — placement decided once

next → R8 chapter

One box to a fleet is one idea repeated at two layers. Pinterest went from eight servers to a sharded fleet of fifty billion Pins by deciding placement up front — the shard in the Pin’s id, the shard in PinLater’s job descriptor — so a new shard is configuration, never a migration. The echo bus does the same with the {q} hashtag: a queue’s slot is a function of its name, computed client-side, fixed for the life of the queue, so the cluster grows without re-homing a key. And PinLater’s Redis → MySQL durability turn is the same ceiling the persistence floor answers with a dial. The next page is the chapter landing — R8 continues to the Twitter/X case study (R8.04, specified) and the production capstone.

§6

References

sources · related

Sources

Related in this course