<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>SQLite - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/sqlite/</link><description>SQLite - Tag - Tracy Atteberry</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>tracy@magicbydesign.com (Tracy Atteberry)</managingEditor><webMaster>tracy@magicbydesign.com (Tracy Atteberry)</webMaster><lastBuildDate>Tue, 10 Nov 2026 00:00:00 +0000</lastBuildDate><image><url>https://tracyatteberry.com/images/feed-icon.jpg</url><title>SQLite - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/sqlite/</link></image><atom:link href="https://tracyatteberry.com/tags/sqlite/" rel="self" type="application/rss+xml"/><item><title>How many reactions per second can Speechwave handle?</title><link>https://tracyatteberry.com/posts/sw_performance/</link><pubDate>Tue, 10 Nov 2026 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/sw_performance/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/sw_performance/concert.jpg" referrerpolicy="no-referrer">
            </div><h1 id="how-many-reactions-per-second-can-speechwave-handle">How many reactions per second can Speechwave handle?</h1>
<p>A user recently sent us this question:</p>
<blockquote>
<p>How many reactions can the audience send at once before the emoji display slows down or drops some of them?</p>
</blockquote>
<p>It&rsquo;s a fair question to ask before you put an app in front of a few hundred
people at a conference. We haven&rsquo;t had a reported issue yet, but I realized I
had never actually measured it. So I did, and here are the results.</p>
<p>If you haven&rsquo;t seen it, <a href="https://speechwave.live" target="_blank" rel="noopener noreffer ">Speechwave</a> is a live emoji
reaction app for talks. The audience taps emoji on their phones, and a Chrome
extension floats them over the speaker&rsquo;s slides. The speaker gets analytics afterward.</p>
<h2 id="great-expectations">Great expectations</h2>
<p>I already knew from developing the code that each audience member is limited to
one reaction every 3 seconds. So the most any room can send is the number of
people divided by 3, per second. A 300-person room tops out at 100 reactions
per second, and only if everyone taps the instant their button comes back,
<em>and</em> that&rsquo;s if you get 100% room participation.</p>
<p>That gives us our theoretical maximum, but tells us nothing about what the
server does at 100 reactions per second.</p>
<h2 id="what-about-sqlite">What about SQLite?</h2>
<p>I next guessed that SQLite could be a weak spot. Every accepted reaction is
saved to the database, and SQLite has a reputation for being a single-writer
database. However, it turns out SQLite can write far more than a few hundred
rows a second.</p>
<p>Measuring it inside the same container used for the rest of the test, it did
about 7,500 inserts per second. It didn&rsquo;t seem like the database was going to
be out limiting factor.</p>
<h2 id="testing-methods">Testing methods</h2>
<p>Wanting the test to look like production, I built the production Docker image
and ran it locally with the same limits as my Fly machine: one CPU, 512 MB of
memory, and a database pool of 2. Then I wrote a small Node script that
simulates an audience. Each fake person loads the talk page, joins the LiveView
over a websocket, and taps on a timer. A second connection plays the Chrome
extension and times how long a reaction takes to arrive.</p>
<p>There were two gotchas.</p>
<p>First, Docker&rsquo;s <code>--cpus=1</code> limits how much CPU time a container gets, but the
Erlang VM still looked at my laptop and started eight schedulers. On Fly it
would see one. When I pinned the container to a single core, the numbers got a
worse, but a lot more realistic.</p>
<p>Second, <strong>the load generator was the bottleneck</strong>. Every reaction is sent to every
audience phone, so with 800 fake people one Node process had to read about 200,000
messages per second. It couldn&rsquo;t, and that backed things up on the server in a way
real phones wouldn&rsquo;t. I split the generator across several processes and made it
count messages without parsing them.</p>
<h2 id="results">Results</h2>
<p>I held the audience at a fixed size and raised the total number of reactions per
second until something gave.</p>
<table>
	<thead>
			<tr>
					<th>People</th>
					<th>Reactions/s</th>
					<th>CPU</th>
					<th>Delivered</th>
					<th>Delay (median / p95)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>400</td>
					<td>60</td>
					<td>68%</td>
					<td>100%</td>
					<td>4 / 18 ms</td>
			</tr>
			<tr>
					<td>400</td>
					<td>100</td>
					<td>99%</td>
					<td>100%</td>
					<td>60 / 116 ms</td>
			</tr>
			<tr>
					<td>400</td>
					<td>125</td>
					<td>86%</td>
					<td>84%</td>
					<td>24 / 430 ms</td>
			</tr>
			<tr>
					<td>800</td>
					<td>25</td>
					<td>63%</td>
					<td>100%</td>
					<td>3 / 88 ms</td>
			</tr>
			<tr>
					<td>800</td>
					<td>45</td>
					<td>100%</td>
					<td>100%</td>
					<td>76 / 251 ms</td>
			</tr>
			<tr>
					<td>800</td>
					<td>60</td>
					<td>100%</td>
					<td>82%</td>
					<td>43 / 1,495 ms</td>
			</tr>
	</tbody>
</table>
<p>Of course, the answer depends on how many people are in the room.
With 400 people, things slow down at about 100 reactions per second and start
dropping soon after. With 800 people, the same thing happens at about 45.</p>
<h2 id="showing-everybody-everything">Showing everybody everything</h2>
<p>Here&rsquo;s the part I hadn&rsquo;t initially thought about. The audience page also shows
the floating emoji, so that people can see their reaction appear. That means
every reaction is pushed to every connected phone, not only to the speaker&rsquo;s
laptop.</p>
<p>Here&rsquo;s the handler from the audience LiveView:</p>
<div class="code-block code-line-numbers open" style="counter-reset: code-block 0">
    <div class="code-header language-elixir">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-elixir" data-lang="elixir"><span class="line"><span class="cl"><span class="kd">def</span> <span class="n">handle_info</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="p">%</span><span class="nc">Phoenix.Socket.Broadcast</span><span class="p">{</span><span class="ss">event</span><span class="p">:</span> <span class="s2">&#34;new_reaction&#34;</span><span class="p">,</span> <span class="ss">payload</span><span class="p">:</span> <span class="p">%{</span><span class="ss">emoji</span><span class="p">:</span> <span class="n">emoji</span><span class="p">}},</span>
</span></span><span class="line"><span class="cl">      <span class="n">socket</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:noreply</span><span class="p">,</span> <span class="n">push_event</span><span class="p">(</span><span class="n">socket</span><span class="p">,</span> <span class="s2">&#34;new_reaction&#34;</span><span class="p">,</span> <span class="p">%{</span><span class="ss">emoji</span><span class="p">:</span> <span class="n">emoji</span><span class="p">})}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>Every audience member runs that for every reaction. So the work per second is the
number of people times the number of reactions per second, which grows with the
square of the room. In my runs one core topped out at about 35,000 to 40,000 of
those pushes per second, and both of my break points were right there:</p>
<ul>
<li>400 people × 100 reactions/s = 40,000</li>
<li>800 people × 45 reactions/s = 36,000</li>
</ul>
<p>That gives us a rule of thumb: <strong>total reactions per second should stay under about
35,000 divided by the number of people</strong>. A room of 1,000 can handle about 35 reactions
per second. A room of 200 can handle about 175, though it can&rsquo;t reach that, because the
3-second limit caps it at about 67.</p>
<p>Real audiences don&rsquo;t tap constantly. If each person reacts about once every 30
seconds, 800 people send 27 reactions per second, and that ran fine at 55% CPU.
Raise it to 1,200 people at the same pace and the core saturates. Obviously, we
could handle higher loads if we assume less than 100% participation.</p>
<h2 id="what-failure-mode-looks-like">What failure mode looks like</h2>
<p>We want things to degrade gracefully. When the core is saturated, the database insert
inside someone&rsquo;s LiveView waits too long for a pool connection and raises. That
crashes the process, so the reaction is lost, and the phone has to reconnect.
While it&rsquo;s not ideal, the audience is unlikely to notice a few lost reactions.
However&hellip;</p>
<p>Memory was the other ceiling. Each connected person costs about 0.23 MB on top of a
baseline around 160 MB. A 3,200-person run was killed for running out of memory.
That&rsquo;s not something we&rsquo;ve seen in production yet, but it wouldn&rsquo;t be great.</p>
<h2 id="how-we-can-make-it-even-better">How we can make it even better</h2>
<p>The obvious fixes are to batch the pushes to audience phones (one message every
quarter second instead of one per reaction), to batch the database writes, and
to catch an insert failure so one timeout doesn&rsquo;t take a page down with it. The
first one attacks the primary issue. Of course, if we need to, we can always
scale our servers at Fly as well.</p>
<p>Caveats: These were single runs, so the edges are fuzzy: one 800-person run
lost 5% of its reactions at a rate where a harder run lost none, so there&rsquo;s
some variance here. Fly&rsquo;s shared CPU can be throttled harder than a pinned core
on my laptop, so the real limits may be lower. Also, I only measured the server.
Whether the emoji on the speaker&rsquo;s screen keep up at these rates is a separate
test. For <em>really</em> large talks we may need to throttle/summarize the reaction
stream going to the speaker&rsquo;s broswer extension.</p>
<h2 id="so-in-summary">So in summary&hellip;</h2>
<p>For a typical talk with a few hundred participants, life should be good. For a
thousand active particpants (congratulations!), reactions might start to drop
above roughly 35 reactions per second across the whole audience, which is what
you&rsquo;d see if everyone tapped about once every 30 seconds and then something
exciting happened in the talk. That&rsquo;s a good problem to have, and now we know
where the edge is. Even better, we know how to push the edge out futher if we
need to.</p>
]]></description></item></channel></rss>