Skip to the content

Projects05 / 07 · render service · 2022–present

ImageMaker 05

Reply “quote” to any message and get a designed image back in about a second — and the same service quietly renders for a second product.

Tech lead · architect · devops
Skip to the engineering
In plain words

People quote each other with screenshots. This replaces that with one word.

In group chats people quote each other with screenshots — ugly, unreadable, and impossible to share outside the app. ImageMaker replaces that with one word: reply “quote” to any message and the bot returns a typeset image with the text, the author’s avatar, their name and the date on a matching background.

The decision that doubled its value. Instead of leaving it a standalone toy, I turned it into the shared rendering engine for the company’s other product. The same service now answers its own audience and three environments of a different bot on another physical server — one codebase, one team, two product lines.

In production since December 2022, 118 thousand new users in the last year alone, and the entire hosting bill is twelve dollars a month. That number is not a brag about frugality; it is the reason half the engineering below exists.

For engineers

Typography by measurement, and 586 MB of render on a 1.9 GB box

Dual-mode runtime, binary-searched type sizing, cascading LLM fallback, every memory kill contained in its own cgroup.

Dual-mode runtime

One codebase answers Telegram webhooks and acts as an RPC consumer — correlation-id request-reply over RabbitMQ, images shipped as zlib-compressed base64. The same render path serves its own users and three environments of another product, so a bug fixed here is fixed twice.

Asymmetric queue federation across two servers

The render host pulls jobs from the app host and writes replies locally; the app host pulls them back. No open HTTP ports between machines, no direct coupling — and the common failure mode is documented for whoever is on call. A dead host degrades the feature instead of cascading into the other product.

Typography by measurement, not heuristic

Three independent binary searches size the quote, the name and the date — the quote against real Pango render measurements, the name and date against ImageMagick’s caption renderer — bounded by a pre-tuned ceiling table with a fast path, plus a fourth, Pango-backed pass so the longest single word cannot overflow the box. No magic font-size constants, and no “usually fits”.

586 MB of peak render on a 1.9 GB box

Layered ImageMagick resource limits so a pathological input cannot take the host down; glibc malloc arena tuning (MALLOC_ARENA_MAX=2 in the systemd drop-in) because RMagick holds memory outside Ruby’s GC; disciplined image destruction in every ensure block; rendering moved out of the web process into Sidekiq, after which the web process stopped being killed at all — and every kill elsewhere stays inside its own cgroup.

Cascading LLM fallback

The bot picks the background itself: AI reads the quote and chooses a theme. Four models are tried in order, each in its own rescue, output validated against a whitelist — so no model failure can break a render and the user never sees an error. The AI is a nice-to-have wearing a seatbelt, not a dependency.

Formatting survives · author identity

Telegram entities become Pango markup: filtered by intersection with the quoted fragment, applied in reverse order so tag insertion doesn’t shift later indices, then escaped in three passes. Forwards, hidden senders, channel and supergroup posts, anonymous admins and service bots each resolve differently — with a fallback chain down to one of 17 placeholder avatars and a circular crop drawn as a mask.
+118k
users in the last year
0
OOM kills in the web process since the render moved to Sidekiq
18k
largest chat
179
spec blocks written · RSpec and RuboCop in CI
Worth stating: the interesting engineering here is not scale — it is doing image rendering, LLM calls and cross-server RPC inside twelve dollars a month without the user ever seeing a failure.

Who worked on it · 5 contributors

Danyil Shkoropad

tech lead · architect · devops · 2022–2026
  • The dual-mode runtime, and the decision to make it the shared render engine for a second product.
  • Drove peak render to 586 MB on a 1.9 GB box, and the web process stopped being killed at all.

Vladyslav Fomenko

fullstack · 2024–2025
  • Moved the quote rendering into this service and built the API endpoint the second product calls.
  • Part of the type measurement and the text formatting the rendered image depends on.

Oleksandr Shemberko

backend · 2025–2026
  • The /qx command — ready-made author quotes, which is the half of the service the second product calls.
  • Per-user quote settings, AI-picked backgrounds and the admin /stat command.

Mykhail Yun

co-founder · product · 2022–2026
  • The conversation that turned a standalone toy into the shared render engine for a second product.
  • Positioning and the requirements for what a quote card has to contain.

Claude

pull request review · 2026
  • Reviews the render path, where a leak shows up as an OOM two days later.
  • Checks every image object is destroyed in an ensure block.

Read to the end and want to talk it over?

rubyco.in