Five million sends: what breaks, and what a recipient sees when it does
By Danish Mohammed, founder. 8 min read
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. This is the scale chapter of choosing a dynamic image and countdown timer vendor for an enterprise email programme, and every figure in it is either read from the code on 23 September 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.
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.
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 once the edge Worker’s route is live, expired requests can be collapsed to one address for the whole template.
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:
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, before and after its route is live
Today every open reaches the one server through Cloudflare’s proxy. Cloudflare’s cache in front of it absorbs repeats for as long as the headers allow, 30 seconds for a live countdown, and everything else lands on the server.
The edge Worker is deployed and waiting for its route. Once that is live it verifies the signature before anything reaches the server, answers repeats from Cloudflare’s cache under the exact signed address, rewrites expired requests that carry their own deadline to one address per template, and keeps a last-known-good copy of every render for outages. Its header comment expects the large majority of traffic to be answered there. That is a design target, and it has not been measured, because no production traffic has passed through it yet.
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.
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.
| 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.
Questions and answers
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 $19 a month for 250,000.