---
title: "How we recreated r/place with 10 lines of SQL"
excerpt: "Two developers built a data-intensive real-time app in half an hour."
authors: "Alberto Romeu"
categories: "I Built This!"
createdOn: "2023-09-25 00:00:00"
publishedOn: "2022-07-06 00:00:00"
updatedOn: "2023-09-26 00:00:00"
status: "published"
---

<p>On April 2nd, 2022 <a href="https://www.macrumors.com/2022/04/02/reddit-is-down-during-r-place/">reddit temporarily went down</a>. Rumor has it this was caused, at least in part, by the second r/place event. If you’re unfamiliar with r/place, you can read up on it <a href="https://en.wikipedia.org/wiki/R/place">here</a>. But the tl;dr is that the 2022 event was a social experiment where millions of users from across the internet vied for pixel dominance, placing 160M timestamped pixels onto a common canvas over the span of 4 days. The timelapse is mesmerizing:</p><figure class="kg-card kg-embed-card"><iframe allowfullscreen="true" frameborder="0" scrolling="no" src="https://www.youtube.com/embed/K5O3UgLG2Jw" title="r/Place 2022 Timelapse"></iframe></figure><p>Building a collaborative drawing canvas seems like a pretty simple project on the surface, but it’s actually really hard, especially at the scale of r/place (As many as 90K concurrent users created up to 8k concurrent write events per second). We recently shared <a href="https://www.tinybird.co/blog-posts/the-hard-parts-of-building-massive-data-systems-with-high-concurrency">8 challenges</a> building high-concurrency, low-latency data apps on massive-scale data, and r/place checks all eight boxes.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://tinybird-blog.ghost.io/content/images/2023/09/62c48719efbf3ac46e85e5b6_PxT85Aht-4VmJufW512yOpdSy2vF8t9clQy8Iuj8s5D_phIhas32I6j5vEfgy_SWH-YJN7FLhftRyv2fV1dkL75Dx7OYXJ2DeH6W_yEHImFIi47L_lm88n0JF0OE3blrxEzeRSD99EOibGQY7w-11.png" class="kg-image" alt="A chart showing user concurrency on the r/place canvas with peaks of over 8,000 concurrent write events" loading="lazy"><figcaption><em>Concurrency peaked at over 8,000 write events per second in the last day of r/place 2022. You can explore this data </em><a href="https://karman.tinybird.co/?datasource=t_2022_place_canvas_history_000000000000&amp;column_name=timestamp&amp;start_datetime=2022-04-01+09%3A18%3A28&amp;end_datetime=2022-04-05+09%3A18%3A28&amp;ls=&amp;r=&amp;g=1&amp;token=p.eyJ1IjogIjUxNDhmMWRiLTgzMTAtNDAzZC04MzJiLTRlNTY0Yzk5MGU2ZCIsICJpZCI6ICI3N2MzNTZiMy02NDNkLTQwZjUtOTU2Mi0wZDAxMDE1YTU1NDEifQ.uuQGgNUQqh7eo7bjk21ObMfUCqzM7hou1wLtOcSaBGc" target="_blank"><em>here</em>.</a></figcaption></figure><p>Recently, I did a <a href="https://youtu.be/yYuzxUOrwOE">live coding session</a> with my coworker Raquel to demonstrate how you could solve this difficult problem. In less than 45 minutes we used Tinybird and some Javascript to recreate the 2022 canvas, plus build a new canvas of our own.</p><p>If you were casually watching the livestream, you may have missed the significance of what happened, so I’ll say it here: <strong>Two people who aren’t data engineers put a data-intensive real-time app into production in about half an hour.</strong> No infrastructure, no backend setup, no nothing. Just two developers and Tinybird.</p><p>I wanted to write this blog to dig into what we did and why it’s so important for developers trying to solve high-concurrency, low-latency problems like r/place.</p><p>Let’s start with the frontend.</p><h2 id="you-don%E2%80%99t-need-kafka-for-events-streaming">You don’t need Kafka for events streaming</h2><p>One thing the canvas has to do is send pixel placement data to a streaming event handler. You might approach this problem with something like Kafka, but with Tinybird, you really just need a (free) account and a few lines of javascript:</p><figure class="kg-card kg-embed-card"><iframe width="100%" src="https://snippets.tinybird.co/XQAAAAIvAgAAAAAAAABBKUqGk9nLKwp6THB8s4RtPZZee-JGs4Qidxo4F1l3jj3KW0MnFUAGytbiCy6QyVrk3x41FmLsRpWaH8W1qCVJEqJEtZEJ_v-sEtwi4f49wRno1h_EuB1ZUBwSvyeSVlB5oY3ZW81oViCxTuzMYkBzal0gBUV6gcFfG7gJu-cC_91wGpjFF8aV5WK-r6Iag3CKf-ah2KXtilsV5InBov7hfdlu0xbKP73ekkJd3TeDcXZXwfYutkkpUzPizOwi7CvNwK9vzy_SNJzZCr7J9WkB8w2cJp-OxYoqDFr7MKzK8vP-tJl8GyYNtOsrrFODpqg57TboaIiR9bPK6NPWBpdOUZx9ixNNnu4asSPrhd4GDp8eH_GnqyElWSlBRWDY-3W4XZB3rtqYmQIUiPdlMdQ1Cv30ydjbpbyvYoXct_UfF15jhzXARJVIOsjEfYqaPop_aOa15JVLyNpWMxjVMma0Ds4114QOfPC47s_EAPdYSNLO3KoEKTGFOE5IweyfZXylJbf_9dpxXA/embed"></iframe></figure><p>The hard part is actually mapping the pixel data to ndjson. From there it’s just a simple POST to the Tinybird /events API.</p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">. <a href="https://twitter.com/tinybirdco?ref_src=twsrc%5Etfw">@tinybirdco</a> event ingestion endpoint is game changing. Instead of using Kafka or SNS, just send analytics events over HTTP (at up to 1000 req/sec) <a href="https://t.co/4aqbBv9Uw0">https://t.co/4aqbBv9Uw0</a></p>&mdash; Jared Palmer (@jaredpalmer) <a href="https://twitter.com/jaredpalmer/status/1526892823173283844?ref_src=twsrc%5Etfw">May 18, 2022</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><h2 id="10-lines-of-sql-for-a-backend">10 lines of SQL for a backend</h2><p>When you start working on a project like this, the frontend feels like the easy part. But that wasn’t the case with Tinybird. I would say that between the two of us, Raquel and I spent probably 80% of our time building the frontend, and the remaining 20% on the backend AND on creating some <a href="https://twitter.com/alrocar/status/1541759795451117568">cool analytical visualizations</a> from the 2022 r/place canvas.</p><p>In fact, the backend was really just 10 lines of SQL written in the Tinybird UI:</p><figure class="kg-card kg-embed-card"><iframe width="100%" src="https://snippets.tinybird.co/XQAAAALGAQAAAAAAAABBKUqGk9nLKwjlHlERNYXw-JRF4mwvWXHWELcc1JLHxTtx8UGjHHh4vIt6dpFSK873ucKFC2apuQaQkn5rzi6_zn10c4yfqS3InruDxqS2cW13GYkE7erdtWI6-AlB_A3b8Ep-cb2XI9VIaWc3MsJK8ROtVJxDwKXttjFqtkfUlCQNi180S36D3oyJITyHvAI7h7S763IgMcfP4rpjVBW7izkv8fdyZnAnuADf4L65ig-usIduYjw11bfryPJM1U20KE2MA5MSVa_Tf1ywJf_x2f2eQD1NvUYVxBuPOyhG9gfJ8P6sx81hu40vLuVpxks7Hpy7xcE9wwS8ZDKSNjpXmqW6Wgp-rqpTQaSw3cQp-OoFk7NZERhhwdTqrXw0BWKZQMLSgVzecPUzUEZwd9-Q6f8_9Yhut5waPDTNCJXjAJ6tkYZK1VVUo8q1-Wx8q754fflImtynbZ67297_EbJWAA/embed"></iframe></figure><p>These 10 lines of SQL took the incoming placement events sent to <code>pixels-table</code> datasource using the Tinybird <code>/events</code> endpoint, got the most recent color placed on that coordinate (using <code>argMax</code>… more below), and - using templating language - defined the parameter <code>start_date</code> so the frontend could request only added data since it’s last request.</p><p>That <code>argMax</code> function alone saved me a lot of time by grabbing the most recent color - based on the max timestamp associated with those coordinates - in a single line. If you’ve ever wanted to do point-in-time SQL queries, like aggregation on one column based on another timestamp column, you’d need to use a window function with vanilla SQL. But with Tinybird (which uses ClickHouse® functions) it’s trivial.</p><p>I augmented this code a bit to let the frontend choose between displaying the active canvas or the historic 2022 r/place event canvas. This was as simple as adding another node to my existing pipe with a <code>historic_date</code> parameter, and using a final node to pull data from either preceding node depending on which parameter was requested. You can see that code <a href="https://github.com/tinybirdco/r-place/blob/master/data_project/endpoints/get_snapshot.pipe">here</a>.</p><p>And then of course as a cherry on top you have the Tinybird secret sauce that turns that final node into a parameterized API endpoint in a single click.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://tinybird-blog.ghost.io/content/images/2023/09/62c48719393db13169482746_FuvbGOAo74WokOBE-ce6QpuQRJ6gFblrQCV_DezX1FRYpC_CyUx02Jc2P-_m35UZ0NkSP-2jxxBu3OdlY2tkj1NPviglu5f8zKaYO0fsDKsqDLPCIq6y6cD8c1O10woa4Zjc9ltyinntOHmbdw-10.gif" class="kg-image" alt="A gif showing how a Tinybird SQL Pipe gets published into an API endpoint with documentation by clicking a button" loading="lazy"><figcaption>Tinybird converts a Pipe node to an API endpoint - with docs - in a click.</figcaption></figure><p>And here is that API being called in the frontend:</p><figure class="kg-card kg-embed-card"><iframe width="100%" src="https://snippets.tinybird.co/XQAAAAK2AgAAAAAAAABBKUqGk9nLKwQVMWn9XrcbEpZ4V0jVATOzRfItrDKiAJXCwINA9rojbgWACYETjNuv182r7DDb_eOMHN70A6K5OIsLx6gcrMhwXUjTr8N8QGZ9PwxpIgLfbgFAVMTKWlbP4s80qlxATDvxaATmsDK2mtP_0YmvqSLcLyN4R9wny2PCZSGbX-KyTimlJvMcH5E_JZlb30P2gLQ1s7suISuu_tQ0AeyFSHmgBhWJhiD_0CJpCSOH0f9THApU5xyju0uKbmq2LzqDlbZuzgeGSoOgoa9LI-Eb0UEBvYDZYG_1Y37sdm5R0Bg8qa9K5NcPjDxIszZXhPww8kR-VOu0J853AQLIq41gbyXLQJwgadTFJ6IXSxzk0OA6dylmE-Gr3fF_YcsMjfDhEK_9rJlbO92LthpxOB7_1U6GqlExEEVK5P6sjmmYaHQQ3sSGMYQQyun4FZYkW9ZF_9iev9yk4nuyKjBzhgDqgfctZVhtkMyy8OjNjD48tlD92P8SR0W8Sz6oZCPoJoBnnW_zIToD86eQSbjaDtvyWT82N8j___KThfo/embed"></iframe></figure><p>Just to put things into perspective: The frontend app is already super simple and compact: 41 lines of HTML + 181 lines of javascript + 121 lines of CSS = 343 total lines of code.</p><p>The entire backend was written in 58 lines of code in Tinybird.</p><h2 id="data-as-code">Data as Code</h2><figure class="kg-card kg-embed-card"><iframe width="100%" src="https://snippets.tinybird.co/XQAAAAKKAAAAAAAAAABBKUqGk9nLKvQ4GKjULb0PECosnRXByLR9GIIh2STS_YW3z9S0-Hju7Q36gMZeVFPu3WW2LWH8NQC60Tf3hkItt_Hmoh2PmGqhcEZGLAKUC7A__GlUJgsJNapPU39ghnO5Atdq3p3FTzIeZBjWoqIApGt8bpgBU-cxISEW__-llIAA/embed"></iframe></figure><figure class="kg-card kg-embed-card"><iframe width="100%" src="https://snippets.tinybird.co/XQAAAALJAAAAAAAAAABBKUqGk9nLKzhOU2PRENkZU1c_zQRr079On6E2rj45TDijAPMZgWpx40wHfWBrmMbo5nFvOatq02ed50hiDZBKoR4yD_NWrLus9VP6g-24m3--VXbK_BIoFmx-P8I4np8kIetuT50Q0mypzGHPenw5v8BYlN-Lm3oboHcnCQpbsCsaAoawUOJNlha30oOkFPPTZ0mH8BJgNlncTIe6TEKtqnlNeRkFwpPn0Ch3CTif_s5kgA/embed"></iframe></figure><figure class="kg-card kg-embed-card"><iframe width="100%" src="https://snippets.tinybird.co/XQAAAAJ7AAAAAAAAAABBKUqGk9nLKzhWwZLGe3hQuZFrSLIPSHH79nAm4jaZ_NtC2Ws11rqXpqUncTDwP_SdPu39wU-lbB9L--Sp8K_U1tijuijUD-4TkDzA9okp7UcGUk4yRW2VsDAddRb5uDOMn-AVngEDoc9n1cOw97wPKRykSRv__bqYAA/embed"></iframe></figure><p>‍</p>
