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).
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.
Eight servers, then over capacity
Sharding Pinterest · the growth wallPinterest’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.
PinLater’s free sharding
Open-sourcing PinLater · the descriptorPinLater, 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.”
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.
The BCS echo — placement is a function of the name
echo_mq/lib/echo_mq/keyspace.ex · slot/1The 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 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:
# 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.
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.
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.
The durability turn
Open-sourcing PinLater · Redis → MySQLThe 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 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.
Recap — placement decided once
next → R8 chapterOne 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.
References
sources · relatedSources
- Pinterest Engineering — Sharding Pinterest: How we scaled our MySQL fleet — eight servers, over capacity by Sept 2011, the early-2012 sharded fleet of 50B Pins / 1B boards, and the move-cost principle.
- Pinterest Engineering — Open-sourcing PinLater — the free sharding scheme, the 500+ queues / 6M+ jobs-per-minute scale, and the Redis → MySQL durability turn.
- GitHub — pinterest/pinlater — the Thrift async-job service; the
[queue_name][shard_id][priority][local_id]descriptor and the per-shard QPS backend guidance. - Valkey — Cluster specification — CRC16 modulo 16384 hash slots and the
{…}hashtag the bus uses to co-locate a queue’s keys.
Related in this course
- R8.03 · Pinterest: task queues & partitioningthe module hub
- R8.03.2 · List-based reliable queuesthe previous dive — the processing list, the lease, no lost job
- R8 · Production & Operationsthe chapter
- /echo-persistencethe durability dial the Redis → MySQL turn points to
- /echomq/queuethe Queue pillar — jobs, lanes, leases, the keyspace behind the slot