Scale a Thumbnail-and-Watermark Pipeline With Worker Processes
AnthropicSoftware Engineer · OnsiteLast updated
Design a batch pipeline that produces a resized thumbnail with a corner watermark for every uploaded photo, then extend it to run across a bounded pool of worker processes. Tests resource lifecycle, atomic output writes, per-item failure isolation, and what to pass between processes.
A photo-sharing backend needs to turn every freshly uploaded photo into a thumbnail stamped with a small corner watermark, for use in feeds and previews. Start with the single-photo operation, then scale it across a batch using multiple worker processes.
Constraints and assumptions
- Each input photo comes with an explicit job spec: source location, target thumbnail dimensions, a watermark intensity value, and a destination location. Nothing about size, intensity, or destination is implicit or hardcoded.
- Photos are independent of each other, but disk I/O and memory are shared resources across the whole batch.
- One failing or corrupt photo must never take down the batch; it should be reported and the rest should keep going.
- For this exercise, represent an image as a 2D grid of integer pixel values rather than depending on an external imaging library, so the pipeline is self-contained and easy to test; the same design carries over directly to a real library like Pillow.
Part 1 — A correct single-photo operation
Write a function that takes one job spec, reads the source image, resizes it to the target dimensions, stamps the watermark into its bottom-right corner, writes the result to the destination, and returns a result or error record — never raises past its own boundary.
def process_image(spec: dict) -> dict:
"""spec has keys: id, src_path, dst_path, target_size (w, h), watermark_value.
Returns {'id': ..., 'status': 'ok'} or {'id': ..., 'status': 'error', 'error': str}."""
Think about what happens if the pipeline crashes mid-write: should a half-written output file ever be mistaken for a finished one?
Part 2 — Scale it across worker processes
def run_pipeline(specs: list[dict], max_workers: int) -> list[dict]:
"""Process every spec, bounded to at most max_workers processes in flight, and
return one result/error record per spec, regardless of individual failures."""
Decide what crosses the process boundary to each worker, and why that should be small.
What this should cover
- Explicit transform order (resize, then watermark) and no hidden default dimensions.
- Output publication that can't be confused with a truncated, in-progress file.
- A bounded worker pool, not one process per photo.
- Small, serializable job descriptions passed to workers, rather than shipping decoded pixel buffers through interprocess communication.
- One photo's failure producing an error record, not an exception that kills the batch.
Hints
Hint 1
Resist the urge to pass decoded pixel data into a worker — pass the job spec (paths and parameters) and let the worker do its own reading, since paths are tiny and pixel buffers are not.
Hint 2
Writing straight to the destination path risks a reader seeing a partial file if the process dies mid-write; write to a temporary path first and rename only once the write is complete.
Hint 3
A process pool from the standard library (not a manual process-per-item loop) gives you the bounded concurrency for free — the bound is just the pool size.
Hint 4
Catch exceptions at the boundary of the per-image function itself, not in the orchestration code, so one bad image can never propagate up and cancel everything else in flight.
Follow-up questions
- How would you size max_workers automatically based on available CPU cores and memory, instead of a fixed constant?
- How would you add a per-image timeout so one pathologically large photo can't stall the whole pool?
- If this pipeline needs to run across multiple machines, not just multiple processes on one machine, what would you change first?
- How would you avoid two concurrently running batches racing to write the same destination path?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.