Menu

Personalization at scale: what breaks at five million opens, and what a reader sees

Warehouse worker pulling a pallet jack down an aisle of stocked orange shelving
Photo: Tiger Lily, Pexels

Personalization at scale means giving every person on a very large list content that is theirs, and producing it fast enough that nobody waits. In email, choosing the values is your platform’s job. Producing one image per recipient and drawing it at open is where volume breaks things, so this page shows the ceilings in EmailTimer.app’s code and what a reader sees when one trips.

That is the production side of image personalization for email. At five million sends, the send itself is not what strains a dynamic image service. The opens are, because each one is a request that has to be answered while the recipient waits. EmailTimer.app meets that with a per-second cache for countdowns and hard ceilings on everything else, and a request past a ceiling gets the “image unavailable” card for 30 seconds, never an error. None of it has been load-tested on production hardware yet.

That last sentence matters more than any number below, so it comes first. It is also the scale chapter of choosing a dynamic image and countdown timer vendor for an enterprise email program, and every figure in it is either read from the code (on 23 September 2026, checked again on 4 November 2026) or was measured on a Mac. Where a figure is a Mac figure, it says so.

Five million is not an arbitrary number here. It is the monthly open allowance of the largest published plan on pricing, Agency.

What is personalization at scale?

Braze defines it as “the ability to adapt content, offers, and experiences to each customer” and then goes on to data, segmentation, AI and case studies. That is the decision half: what each person should get. It says nothing about the production half, which is where an email program meets hard limits.

For a personalized email image, the work splits four ways:

What is personalization at scale?
Job Who does it What limits it
Choosing each person’s values Your email platform or customer data platform The quality of your data
Minting one image address per recipient A recipient list upload, or the API 500,000 rows per list; 10,000 addresses per API call
Drawing each image when it is opened The image service The ceilings below
Sending the email Your email platform Its own sending limits

The second row is the one teams forget to plan for. A CSV of up to 500,000 rows becomes a recipient list, and its export is a CSV with one signed image address per row. From your own systems, POST /urls/batch signs up to 10,000 addresses per call, and the API allows 600 requests a minute on its two bulk routes, against 60 on everything else. So a million recipients is 100 batch calls, well inside one minute’s allowance. Both routes are on Growth and Agency, and image mail merge walks through the list route. What can go into each address, and what cannot, is on hyper personalization in email.

The third row is the rest of this page.

Five million sends is not five million renders

Spread evenly over an hour, five million opens is 1,389 requests a second. Over a day, it is 58. Real opens are not even: they bunch in the first minutes after a send, and Apple Mail with Mail Privacy Protection fetches images at delivery rather than when a person opens the message, so a large Apple audience arrives as the send is still going out.

Shoppers in winter coats crowding round a lit shop window
Opens arrive the way a crowd reaches a shop window when the doors open: bunched into the first minutes after a send, not spread evenly across the day. Photo: Konstantin Khrustov, Pexels

What each request costs depends on what the image is.

A countdown with one deadline for everybody is the cheap case. Everyone who opens in the same second is sent byte-for-byte the same GIF, so the service assembles it once per second and keeps it for 90 seconds. Requests that arrive in the same worker process while it is being made wait for that one render rather than starting their own. A thousand opens in one second cost a handful of renders, one per worker at most, and the rest are cache reads.

An image personalized per recipient is the expensive case, and no cache can make it cheap. A name drawn into a banner is a different picture for every recipient, so every first open is a fresh render. The service keeps each personalized render for one hour, which catches a recipient opening twice or a proxy fetching again, and nothing longer, for a reason covered in the Redis section below.

An expired timer is cheap again. Every recipient gets the same frame, shared caches keep it for a week, and the edge Worker’s code collapses expired requests that carry their own deadline to one address for the whole template.

The ceilings, as they are in the code

One open, and the ceiling at each step Four steps from left to right. The mail client or its proxy, such as Gmail's image proxy, Apple's prefetch or a corporate network, where one network address can be thousands of recipients. Cloudflare, where the edge Worker has been live on the image route since 26 September 2026: it stops forged addresses and answers repeats from its cache. The image service on one server, with a burst of 5,000 requests per network address, 10,000 per template, 64 renders in flight and a 5 second deadline, each per worker process. Redis, holding the countdown GIF for 90 seconds, a personalized render for an hour and a shared render for 7 days, on one 1 GB instance shared with sign-in. Below: when any ceiling says no, the recipient gets the service card at HTTP 200, and shared caches keep it for 30 seconds. Mail client or proxy Gmail's image proxy, Apple's prefetch at delivery, a corporate network One network address can be thousands of recipients Cloudflare Edge Worker, live since 26 September 2026: stops forged addresses, caches Live countdown: 30 s Expired: 7 days A forged address stops here Image service Per worker process 5,000 burst, 500 a second per network address 10,000 burst, 1,000 a second per template 64 renders in flight 5 second render deadline Redis Countdown GIF: 90 s Personalized render: 1 hour Shared render: 7 days One 1 GB instance, shared with sign-in Any ceiling says no: the service card, at HTTP 200 Shared caches keep it 30 seconds, so the next open after the spike gets a real render
Drawn from apps/img/src/limits.ts, cache-headers.ts, templates.ts and redis-roles.ts on 23 September 2026, and checked against the code again on 4 November 2026, when the edge box was brought up to date.
The ceilings, as they are in the code
Ceiling Value in the code What it stops What the recipient sees
Per network address 5,000 burst, then 500 a second One machine turning into unlimited renders The service card
Per template 10,000 burst, then 1,000 a second One captured address replayed from everywhere at once The service card
Renders in flight 64 A queue whose head has already given up The service card
Render deadline 5 seconds A render stuck on storage or a font The service card
Pending usage records 250,000 A stalled meter filling the Redis that holds sign-in Nothing. Usage is under-counted instead

Every figure in the first four rows is per worker process. The service forks one fewer worker than the machine has cores, which the code notes is three on the production server, so the real ceilings there are three times the table: 15,000 and 1,500 a second per address, 30,000 and 3,000 a second per template. They are sized to be enormous on purpose. One network address is routinely a whole company behind a corporate network, or a share of Gmail behind Google’s proxy, and throttling one of those would hand the card to every recipient behind it.

Here is what the card looks like, rendered by the service itself:

A white card on light grey reading image unavailable, with emailtimer.app beneath it

It is the same picture for every refusal, it keeps the layout of the email, and it does not count as an open. A cache holds it for 30 seconds, so a spike costs the recipients who opened inside it, and the next open after that renders. What a timer shows when it is not counting down lists every cause, and when the image service is down covers the failures that are not about volume.

What breaks first is not the images

The part most likely to fail under a very large personalized send is the Redis instance, and the symptom would not appear in the campaign at all.

Production runs one Redis with 1 GB and a policy of refusing writes when full. That same instance holds every signed-in session, the signing keys, the recipient records and the render caches. A render cache for a personalized campaign holds one image per recipient, around 20 KB each, so 500,000 recipients at a week each would be about 10 GB of writes into a 1 GB instance. When it filled, the image side would carry on, because it drops a failed cache write by design. Sign-in would not.

Three things in the code stop that from happening:

  • A personalized render is kept for one hour, not a week. An image that is the same for every recipient keeps the full week, because it is one entry.
  • The queue of usage records is capped at 250,000, about 50 MB, keeping the newest.
  • The caches can move to a second Redis with its own size and an evicting policy, set by one variable, CACHE_REDIS_URL.

The third is written and not switched on. Today everything is on the one instance. The order for splitting it is in the runbooks, and it has to be done with the metering job in the same deploy, or usage stops being counted without any error.

The edge Worker in front of the server

Since 26 September 2026 every open reaches the edge Worker first. It was tested end to end when the route went live. A correctly signed address passed the Worker’s check, was fetched from the server, and the repeat was answered from the Worker’s cache. A forged one was stopped by the Worker with a blank one-pixel image and never reached the server. A request sent around the Worker, straight to the server, was refused.

Its code does more than that. It rewrites expired requests that carry their own deadline to one address per template, keeps a last-known-good copy of each render for outages, and caches every answer under the exact signed address, so a personalized image is only ever served to the address it was drawn for. Its header comment expects the large majority of traffic to be answered there. That is a design target. Nobody has measured the share under a campaign-sized send.

Crossing the plan limit in the middle of a send

At 110 percent of a plan’s monthly opens, new renders carry a small line of grey text in the corner, and nothing else changes: not the countdown, not the name, not the layout.

A sale countdown reading 01 days, 11 hours, 59 minutes, with the small grey text Powered by EmailTimer.app in the bottom right corner

There is one scale effect worth knowing. The mark is part of the image, so the moment a workspace crosses the line, the service’s cached renders for it stop matching and are drawn again. Renders carrying the mark are recorded and not billed, because the workspace is already over its allowance. Plan quota and the watermark has the rule, and how opens are counted defines what one open is.

What has been measured, and what has not

Every timing below comes from the engine run in a single process on an M-series Mac with Node 22, in September 2026. None was taken on the production server, and none under concurrent load. A countdown already in the per-second cache is a Redis read, far cheaper than any row here.

What has been measured, and what has not
Case Measured on a Mac
Countdown assembled from cached parts 4.33 ms of CPU, so one process tops out near 230 a second
Design redrawn, digits cached 10 ms
Design and digits both redrawn 15.1 ms
First render in a new process, fonts included 84.8 ms

The production server’s processors are slower per core than that Mac, and it shares its cores with the database and the rest of the product. An audit in September 2026 estimated the same warm render at 11 ms there. That is an estimate from a ratio, not a measurement. The load test that would replace it, on a throwaway server rather than the one serving customers, has not been run.

So the honest position for a buyer is this: the limits are written, tested and sized, the caches are in place, and nobody has yet watched five million opens arrive. If your first send is that size, ask for the load test result before the send, not after it. We would rather be asked than find out in your campaign.

For the data side of the same request, what each hop sees and keeps, read where recipient data goes when a personalized image renders. For why none of these requests calls out to a live data feed, which would add a dependency to every one of them, see real time personalization.

Questions and answers

What is the difference between personalization and personalization at scale?

Personalization is deciding what one person should see. Personalization at scale is doing that for every person on a large list and still producing and delivering each version on time. In email images, the second half comes down to minting an address per recipient and rendering each one when it is opened.

How many personalized image addresses can I create at once?

Up to 500,000 rows in one recipient list upload, or up to 10,000 addresses per API call, with 600 calls a minute allowed on the bulk routes. Both are on the Growth and Agency plans.

Can one campaign slow down another customer’s images?

It can share the same server, so yes, in principle. The per-template ceiling is there to stop one campaign from taking the whole service: past 3,000 renders a second on one template, across three workers, that template gets the card for 30 seconds and every other template carries on.

Does Gmail’s image proxy count as one recipient for the rate limit?

Each proxy network address is one identity, and that is why the per-address ceiling is set at 1,500 a second across three workers rather than something a single person could reach.

What is the largest send that has been tested?

None at this scale. The end-to-end tests drive real services with real renders, but not at campaign volume, and there are no customers yet whose sends could stand in for a test.

Sources

Related

Put a countdown in the campaign you are writing now

The free plan covers 5,000 opens a month with no card. Starter is $24 a month for 250,000.