<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Trung Duong</title>
    <description>The latest articles on DEV Community by Trung Duong (@trungdlp).</description>
    <link>https://dev.to/trungdlp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2812367%2F65501929-83d3-45c0-81fc-be22d98865cc.png</url>
      <title>DEV Community: Trung Duong</title>
      <link>https://dev.to/trungdlp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/trungdlp"/>
    <language>en</language>
    <item>
      <title>Correctness Has a Price: We Benchmarked Fair Leaderboards</title>
      <dc:creator>Trung Duong</dc:creator>
      <pubDate>Fri, 31 Jul 2026 06:44:23 +0000</pubDate>
      <link>https://dev.to/trungdlp/correctness-has-a-price-we-benchmarked-fair-leaderboards-3ia8</link>
      <guid>https://dev.to/trungdlp/correctness-has-a-price-we-benchmarked-fair-leaderboards-3ia8</guid>
      <description>&lt;p&gt;Engineering posts often end with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The new design is correct, scalable, and fast.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fast compared with what?&lt;/p&gt;

&lt;p&gt;When we changed &lt;a href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;Podium&lt;/a&gt; so tied players&lt;br&gt;
rank by arrival time instead of player ID, we added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Lua script;&lt;/li&gt;
&lt;li&gt;a per-leaderboard sequence;&lt;/li&gt;
&lt;li&gt;a public-ID mapping;&lt;/li&gt;
&lt;li&gt;a second sorted set for ascending order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That design is fairer. It is also impossible for it to be free.&lt;/p&gt;

&lt;p&gt;So we built two benchmark layers: direct Redis strategy benchmarks to isolate&lt;br&gt;
the data-model cost, and end-to-end HTTP benchmarks to show what users actually&lt;br&gt;
experience.&lt;/p&gt;

&lt;p&gt;We are publishing the results, including the regression, because performance&lt;br&gt;
claims are useful only when readers can inspect the workload and reproduce the&lt;br&gt;
measurement.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/TeneficGames" rel="noopener noreferrer"&gt;
        TeneficGames
      &lt;/a&gt; / &lt;a href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;
        podium
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      High-performance, Redis-backed leaderboards for games and competitive applications.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Podium&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/TeneficGames/podium/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/TeneficGames/podium" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5c8cf6e20a38446e700169d3b28728ecf687ce62ee8e8501850b6c69c562cad0/68747470733a2f2f636f6465636f762e696f2f67682f54656e6566696347616d65732f706f6469756d2f6272616e63682f6d61696e2f67726170682f62616467652e737667" alt="codecov"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/TeneficGames/podium/docs/assets/podium-hero.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FTeneficGames%2Fpodium%2FHEAD%2Fdocs%2Fassets%2Fpodium-hero.webp" alt="Podium high-performance distributed leaderboard"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;High-performance, Redis-backed leaderboards for games and competitive
applications.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Podium provides ready-to-run HTTP and gRPC APIs for scores, ranks, seasons, and
player-relative views. It is designed for backend teams operating large fleets
of independent leaderboards without provisioning each leaderboard in advance.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fair, deterministic ordering when scores are equal.&lt;/li&gt;
&lt;li&gt;Single and bulk score updates, including multi-leaderboard fan-out.&lt;/li&gt;
&lt;li&gt;Standalone Redis and real Redis Cluster integration coverage.&lt;/li&gt;
&lt;li&gt;Deploy one multi-architecture OCI image with Docker, containerd, Kubernetes
or another OCI-compatible runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium#quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt; · &lt;a href="https://github.com/TeneficGames/podium#performance" rel="noopener noreferrer"&gt;Performance&lt;/a&gt; ·
&lt;a href="https://github.com/TeneficGames/podium/docs/API.md" rel="noopener noreferrer"&gt;API&lt;/a&gt; · &lt;a href="https://github.com/TeneficGames/podium/docs/overview.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; ·
&lt;a href="https://github.com/TeneficGames/podium/charts/podium/README.md" rel="noopener noreferrer"&gt;Helm chart&lt;/a&gt; ·
&lt;a href="https://hub.docker.com/r/trungdlp/podium" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://github.com/orgs/TeneficGames/packages/container/package/podium" rel="noopener noreferrer"&gt;GHCR&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quickstart&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Start Redis 8.2 and the latest stable Podium image:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker network create podium
docker run --detach --name podium-redis --network podium redis:8.2-alpine

docker run --detach --rm --name podium \
  --network podium \
  --publish 8880:8880 \
  --publish 8881:8881 \
  --env PODIUM_REDIS_HOST=podium-redis \
  --env PODIUM_REDIS_PORT=6379 \
  trungdlp/podium:latest start&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Verify the service:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl http://localhost:8880/healthcheck&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;WORKING
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Submit two equal scores:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl --request&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The results were reassuring in some places and uncomfortable in one. Both were&lt;br&gt;
useful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Start with the correctness contract
&lt;/h2&gt;

&lt;p&gt;The benchmark cannot tell us whether a faster implementation is acceptable.&lt;br&gt;
Tests do that first.&lt;/p&gt;

&lt;p&gt;Every candidate tie-break strategy had to satisfy the same behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alice reaches 100
bob reaches 100
top =&amp;gt; alice, bob

alice submits 100 again
top =&amp;gt; alice, bob

alice changes to 90, then returns to 100
top =&amp;gt; bob, alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The production strategy additionally supports both ascending and descending&lt;br&gt;
boards, atomic increments, TTL cleanup, and concurrent arrivals.&lt;/p&gt;

&lt;p&gt;This ordering matters. A benchmark suite without a behavior gate can crown an&lt;br&gt;
implementation that solves an easier problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Compare representations, not just commits
&lt;/h2&gt;

&lt;p&gt;We tested four strategies:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Plain sorted-set baseline
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ZADD scores &amp;lt;score&amp;gt; &amp;lt;public-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the smallest design. It also lets Redis break equal scores by member&lt;br&gt;
name, so it is a performance baseline rather than a valid solution.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Production encoded-member design
&lt;/h3&gt;

&lt;p&gt;The production model keeps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;descending score index
ascending score index
sequence counter
public ID -&amp;gt; internal token hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An atomic script assigns a fixed-width sequence prefix whenever a member&lt;br&gt;
reaches a new score.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Single-index encoded-member design
&lt;/h3&gt;

&lt;p&gt;This alternative also uses a sequence-prefixed internal member and a lookup&lt;br&gt;
hash, but keeps only one score index. It helps separate the general cost of&lt;br&gt;
encoding members from the cost of supporting both sort directions.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Packed composite score
&lt;/h3&gt;

&lt;p&gt;This version combines the score and tie-break sequence into the sorted-set&lt;br&gt;
score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composite = score * sequence_capacity + remaining_sequence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses less metadata, but the safe score range and sequence capacity must fit&lt;br&gt;
inside Redis's exact integer range for double-precision scores. The benchmark&lt;br&gt;
implementation rejects values outside those explicit limits.&lt;/p&gt;

&lt;p&gt;An alternative is only useful if its constraints appear next to its speed.&lt;/p&gt;

&lt;p&gt;The full harness is in&lt;br&gt;
&lt;a href="https://github.com/TeneficGames/podium/blob/main/leaderboard/database/redis/tiebreak_benchmark_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;tiebreak_benchmark_test.go&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Benchmark operations separately
&lt;/h2&gt;

&lt;p&gt;A single "leaderboard ops/sec" number hides too much.&lt;/p&gt;

&lt;p&gt;The direct Redis suite measures five distinct workloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;insert a member and return rank
change an existing score and return rank
submit an unchanged score and return rank
read one rank
read the top 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This division exposed an important optimization: unchanged score submissions&lt;br&gt;
preserve the existing tie-break token. They should not pay the full cost of&lt;br&gt;
removing and recreating a member.&lt;/p&gt;

&lt;p&gt;It also distinguishes write amplification from read overhead. A design may&lt;br&gt;
make writes substantially heavier while leaving the dominant read paths nearly&lt;br&gt;
unchanged.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measure Redis memory, not only Go allocations
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;go test -benchmem&lt;/code&gt; reports allocations in the benchmark process. It does not&lt;br&gt;
tell us how much memory Redis used for its sorted sets and hashes.&lt;/p&gt;

&lt;p&gt;After the insert benchmark, the harness calls Redis &lt;code&gt;MEMORY USAGE&lt;/code&gt; for every&lt;br&gt;
key owned by the strategy, sums the result, and reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redis-B/member
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In simplified Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;memoryPerMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;redisMemoryUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This metric made the largest tradeoff impossible to miss. In our measured&lt;br&gt;
setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plain baseline:       about 99 Redis bytes/member
production tie-break: about 287 Redis bytes/member
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact number will change with member length, Redis version, allocator,&lt;br&gt;
encoding thresholds, and dataset cardinality. The ratio still tells us that&lt;br&gt;
fair ordering with two indexes has a meaningful capacity cost.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the direct comparison showed
&lt;/h2&gt;

&lt;p&gt;Against the plain sorted-set baseline, the production deterministic tie-break&lt;br&gt;
added approximately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Latency overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Insert and return rank&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change score and return rank&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submit an unchanged score&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read one rank&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read the top 50&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The write cost is unsurprising: the script reads metadata and may update two&lt;br&gt;
sorted sets, a hash, and a sequence.&lt;/p&gt;

&lt;p&gt;The small rank and top-50 overhead is more interesting. Encoding the internal&lt;br&gt;
member does not fundamentally change sorted-set rank lookup, and top-page reads&lt;br&gt;
only need to strip the fixed-width prefix before returning public IDs.&lt;/p&gt;

&lt;p&gt;The memory result is the sharper warning. If your capacity model assumed one&lt;br&gt;
sorted-set entry per member, this correctness feature changes it materially.&lt;/p&gt;
&lt;h2&gt;
  
  
  Then measure through the real API
&lt;/h2&gt;

&lt;p&gt;Microbenchmarks answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What did this data model cost?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Users care about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How long does the operation take through the service?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Podium's end-to-end suite sends sequential HTTP requests through the real API&lt;br&gt;
to a local Redis instance. Each benchmark invocation uses an isolated&lt;br&gt;
leaderboard ID and removes its data afterward. Setup and cleanup remain outside&lt;br&gt;
the timed section.&lt;/p&gt;

&lt;p&gt;Five-run medians recorded on July 30, 2026, with Go 1.26.5, Redis 8.2, and an&lt;br&gt;
Apple M4 Pro included:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;End-to-end HTTP operation&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;Allocated bytes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set one member score&lt;/td&gt;
&lt;td&gt;305 µs&lt;/td&gt;
&lt;td&gt;6.7 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set 50 member scores&lt;/td&gt;
&lt;td&gt;657 µs&lt;/td&gt;
&lt;td&gt;36.1 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get one member rank&lt;/td&gt;
&lt;td&gt;282 µs&lt;/td&gt;
&lt;td&gt;5.4 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get a top-members page&lt;/td&gt;
&lt;td&gt;441 µs&lt;/td&gt;
&lt;td&gt;9.5 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update one member across 100 boards&lt;/td&gt;
&lt;td&gt;3.58 ms&lt;/td&gt;
&lt;td&gt;78.3 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get 501 members&lt;/td&gt;
&lt;td&gt;3.17 ms&lt;/td&gt;
&lt;td&gt;222.6 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are local, sequential measurements, not throughput limits, production&lt;br&gt;
SLAs, or Redis Cluster capacity results.&lt;/p&gt;

&lt;p&gt;That sentence should accompany every benchmark table.&lt;/p&gt;
&lt;h2&gt;
  
  
  The uncomfortable result was the most valuable
&lt;/h2&gt;

&lt;p&gt;The 501-member bulk lookup was approximately 78% slower than the&lt;br&gt;
pre-tie-break implementation.&lt;/p&gt;

&lt;p&gt;It would be easy to hide that result behind the faster common operations.&lt;br&gt;
Instead, it is documented as an optimization target.&lt;/p&gt;

&lt;p&gt;Why did this path stand out?&lt;/p&gt;

&lt;p&gt;The deterministic model cannot query ranks using public IDs directly. It first&lt;br&gt;
resolves each public ID to an internal token, then reads the appropriate score,&lt;br&gt;
rank, and optional TTL. The production Lua script returns aligned triples for&lt;br&gt;
every requested member:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score, rank, ttl
score, rank, ttl
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That preserves missing-member positions and atomic read semantics, but a&lt;br&gt;
501-member request makes the extra work visible.&lt;/p&gt;

&lt;p&gt;This is exactly what benchmarks are for: finding the path where an acceptable&lt;br&gt;
general tradeoff becomes a specific bottleneck.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reproducibility is a feature
&lt;/h2&gt;

&lt;p&gt;The repository exposes the benchmark workflow as Make targets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make bench-redis
make bench-tiebreak
make bench-redis-kill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the HTTP suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make bench-redis
make bench-podium-app
make bench-run
make bench-podium-app-kill
make bench-redis-kill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run count is configurable, and the documentation recommends comparing&lt;br&gt;
medians from multiple repetitions, running old and new builds back-to-back on&lt;br&gt;
the same idle machine, and resetting Redis between implementations.&lt;/p&gt;

&lt;p&gt;See the&lt;br&gt;
&lt;a href="https://github.com/TeneficGames/podium/blob/main/docs/benchmark.md" rel="noopener noreferrer"&gt;&lt;code&gt;benchmark guide&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
for the complete procedure.&lt;/p&gt;
&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;Our benchmarking checklist now looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the behavior every candidate must preserve.&lt;/li&gt;
&lt;li&gt;Keep an intentionally simpler baseline.&lt;/li&gt;
&lt;li&gt;Benchmark operations separately.&lt;/li&gt;
&lt;li&gt;Measure memory in the server, not only allocations in the client.&lt;/li&gt;
&lt;li&gt;Add an end-to-end layer through the real protocol.&lt;/li&gt;
&lt;li&gt;Use isolated data and exclude setup and cleanup from timing.&lt;/li&gt;
&lt;li&gt;Publish the machine, versions, repetition count, and limitations.&lt;/li&gt;
&lt;li&gt;Keep regressions visible, even when the overall design is worth shipping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to prove that a design is "fast."&lt;/p&gt;

&lt;p&gt;The goal is to know what you bought, what you paid, and which bill needs&lt;br&gt;
attention next.&lt;/p&gt;

&lt;p&gt;What correctness feature in your system has the most surprising performance&lt;br&gt;
cost? Share the result or your benchmarking questions in the comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Explore Podium on GitHub&lt;/a&gt;
&lt;/p&gt;

</description>
      <category>performance</category>
      <category>redis</category>
      <category>go</category>
      <category>benchmarking</category>
    </item>
    <item>
      <title>Redis Cluster Won't Shard Your Hot Leaderboard</title>
      <dc:creator>Trung Duong</dc:creator>
      <pubDate>Fri, 31 Jul 2026 06:43:59 +0000</pubDate>
      <link>https://dev.to/trungdlp/redis-cluster-wont-shard-your-hot-leaderboard-2c4p</link>
      <guid>https://dev.to/trungdlp/redis-cluster-wont-shard-your-hot-leaderboard-2c4p</guid>
      <description>&lt;p&gt;"We use Redis Cluster" can mean two very different things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our dataset is distributed across Redis nodes.&lt;/li&gt;
&lt;li&gt;Every individual data structure is distributed across Redis nodes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first can be true while the second is false.&lt;/p&gt;

&lt;p&gt;That distinction matters for leaderboards.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;Podium&lt;/a&gt;, each leaderboard uses&lt;br&gt;
several Redis keys and atomic Lua scripts. Redis Cluster helps us scale a large&lt;br&gt;
fleet of independent leaderboards, but it cannot split one giant sorted set&lt;br&gt;
across primaries.&lt;/p&gt;

&lt;p&gt;We are sharing this architecture because "Redis Cluster scales horizontally"&lt;br&gt;
is true only after you define what the system actually shards.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/TeneficGames" rel="noopener noreferrer"&gt;
        TeneficGames
      &lt;/a&gt; / &lt;a href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;
        podium
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      High-performance, Redis-backed leaderboards for games and competitive applications.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Podium&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/TeneficGames/podium/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/TeneficGames/podium" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5c8cf6e20a38446e700169d3b28728ecf687ce62ee8e8501850b6c69c562cad0/68747470733a2f2f636f6465636f762e696f2f67682f54656e6566696347616d65732f706f6469756d2f6272616e63682f6d61696e2f67726170682f62616467652e737667" alt="codecov"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/TeneficGames/podium/docs/assets/podium-hero.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FTeneficGames%2Fpodium%2FHEAD%2Fdocs%2Fassets%2Fpodium-hero.webp" alt="Podium high-performance distributed leaderboard"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;High-performance, Redis-backed leaderboards for games and competitive
applications.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Podium provides ready-to-run HTTP and gRPC APIs for scores, ranks, seasons, and
player-relative views. It is designed for backend teams operating large fleets
of independent leaderboards without provisioning each leaderboard in advance.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fair, deterministic ordering when scores are equal.&lt;/li&gt;
&lt;li&gt;Single and bulk score updates, including multi-leaderboard fan-out.&lt;/li&gt;
&lt;li&gt;Standalone Redis and real Redis Cluster integration coverage.&lt;/li&gt;
&lt;li&gt;Deploy one multi-architecture OCI image with Docker, containerd, Kubernetes
or another OCI-compatible runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium#quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt; · &lt;a href="https://github.com/TeneficGames/podium#performance" rel="noopener noreferrer"&gt;Performance&lt;/a&gt; ·
&lt;a href="https://github.com/TeneficGames/podium/docs/API.md" rel="noopener noreferrer"&gt;API&lt;/a&gt; · &lt;a href="https://github.com/TeneficGames/podium/docs/overview.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; ·
&lt;a href="https://github.com/TeneficGames/podium/charts/podium/README.md" rel="noopener noreferrer"&gt;Helm chart&lt;/a&gt; ·
&lt;a href="https://hub.docker.com/r/trungdlp/podium" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://github.com/orgs/TeneficGames/packages/container/package/podium" rel="noopener noreferrer"&gt;GHCR&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quickstart&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Start Redis 8.2 and the latest stable Podium image:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker network create podium
docker run --detach --name podium-redis --network podium redis:8.2-alpine

docker run --detach --rm --name podium \
  --network podium \
  --publish 8880:8880 \
  --publish 8881:8881 \
  --env PODIUM_REDIS_HOST=podium-redis \
  --env PODIUM_REDIS_PORT=6379 \
  trungdlp/podium:latest start&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Verify the service:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl http://localhost:8880/healthcheck&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;WORKING
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Submit two equal scores:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl --request&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Here is how the design works, why hash tags are necessary, and where the&lt;br&gt;
scaling boundary really is.&lt;/p&gt;
&lt;h2&gt;
  
  
  One logical leaderboard, five physical keys
&lt;/h2&gt;

&lt;p&gt;A minimal leaderboard can live in one sorted set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;weekly-global -&amp;gt; [(alice, 120), (bob, 100), ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podium guarantees that the first member to reach a tied score ranks higher. To&lt;br&gt;
support that rule in both ascending and descending order, plus member&lt;br&gt;
expiration, a logical leaderboard uses five keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scores       sorted set   descending ranking index
scores-asc   sorted set   ascending ranking index
members      hash         public ID -&amp;gt; encoded internal member
sequence     string       next tie-break sequence
ttl          sorted set   member expiration timestamps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A write may update the score indexes, mapping, and sequence in one Lua script.&lt;br&gt;
A delete may need to clean both indexes, the mapping, and TTL metadata.&lt;/p&gt;

&lt;p&gt;On standalone Redis, those keys can have any names. On Redis Cluster, their&lt;br&gt;
names determine whether the operation is even legal.&lt;/p&gt;
&lt;h2&gt;
  
  
  The cross-slot problem
&lt;/h2&gt;

&lt;p&gt;Redis Cluster divides its keyspace into 16,384 hash slots. By default, each key&lt;br&gt;
name is hashed independently.&lt;/p&gt;

&lt;p&gt;Imagine these keys land on different primaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;weekly-global:scores      -&amp;gt; node A
weekly-global:members     -&amp;gt; node B
weekly-global:sequence    -&amp;gt; node C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Lua script cannot atomically update arbitrary keys across those nodes. Redis&lt;br&gt;
will reject the operation with a &lt;code&gt;CROSSSLOT&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;The fix is a Redis Cluster hash tag. If a key contains &lt;code&gt;{...}&lt;/code&gt;, only the text&lt;br&gt;
inside the braces is used to choose the slot.&lt;/p&gt;

&lt;p&gt;Podium first URL-safe-base64-encodes the leaderboard ID and then constructs keys&lt;br&gt;
like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;podium:{b&amp;lt;encoded-leaderboard-id&amp;gt;}:scores
podium:{b&amp;lt;encoded-leaderboard-id&amp;gt;}:scores-asc
podium:{b&amp;lt;encoded-leaderboard-id&amp;gt;}:members
podium:{b&amp;lt;encoded-leaderboard-id&amp;gt;}:sequence
podium:{b&amp;lt;encoded-leaderboard-id&amp;gt;}:ttl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every key contains the same hash tag, so every key for one leaderboard lands in&lt;br&gt;
the same slot.&lt;/p&gt;

&lt;p&gt;Encoding the external ID matters. A name containing braces or other surprising&lt;br&gt;
characters cannot accidentally control the hash tag syntax. The key-generation&lt;br&gt;
test specifically uses a brace-containing leaderboard name to guard that&lt;br&gt;
boundary.&lt;/p&gt;

&lt;p&gt;The implementation is small:&lt;br&gt;
&lt;a href="https://github.com/TeneficGames/podium/blob/main/leaderboard/database/redis_keys.go" rel="noopener noreferrer"&gt;&lt;code&gt;redis_keys.go&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
Small key-building functions deserve serious tests because changing one can&lt;br&gt;
move production data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Co-location restores atomicity
&lt;/h2&gt;

&lt;p&gt;Once all related keys share a slot, one Redis primary can execute the&lt;br&gt;
leaderboard's Lua script atomically.&lt;/p&gt;

&lt;p&gt;For a score update, that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allocate tie-break sequence
remove stale internal members
write descending index
write ascending index
update public-ID mapping
return final rank
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No other command can observe the operation halfway through.&lt;/p&gt;

&lt;p&gt;Co-location also keeps whole-leaderboard expiration and deletion predictable.&lt;br&gt;
All five keys can receive the same expiration or be removed together without a&lt;br&gt;
cross-slot operation.&lt;/p&gt;

&lt;p&gt;This is the useful Redis Cluster rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;State that must change atomically must share a slot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But co-location has a direct consequence.&lt;/p&gt;
&lt;h2&gt;
  
  
  One leaderboard still belongs to one primary
&lt;/h2&gt;

&lt;p&gt;All five keys for &lt;code&gt;weekly-global&lt;/code&gt; share a slot. One primary owns that slot.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every write to &lt;code&gt;weekly-global&lt;/code&gt; reaches that primary;&lt;/li&gt;
&lt;li&gt;every read of its sorted sets reaches that primary;&lt;/li&gt;
&lt;li&gt;its memory lives on that primary;&lt;/li&gt;
&lt;li&gt;its Lua scripts consume time on that primary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding Redis Cluster nodes does not stripe that leaderboard across them.&lt;/p&gt;

&lt;p&gt;What Redis Cluster &lt;em&gt;does&lt;/em&gt; distribute is different leaderboard IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;weekly-global  -&amp;gt; slot 2,140  -&amp;gt; primary A
daily-eu       -&amp;gt; slot 9,811  -&amp;gt; primary B
clan-42        -&amp;gt; slot 14,002 -&amp;gt; primary C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With enough independent boards and well-distributed names, the fleet spreads&lt;br&gt;
across the cluster. This is a good match for games with regional, seasonal,&lt;br&gt;
event, clan, or per-mode leaderboards.&lt;/p&gt;

&lt;p&gt;It is not a solution for one board whose traffic or member count exceeds a&lt;br&gt;
single Redis primary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Partition exceptional boards at the application level
&lt;/h2&gt;

&lt;p&gt;If one leaderboard is too hot or too large for one node, it needs an&lt;br&gt;
application-level partitioning strategy.&lt;/p&gt;

&lt;p&gt;Possible boundaries include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;region;&lt;/li&gt;
&lt;li&gt;game mode;&lt;/li&gt;
&lt;li&gt;season;&lt;/li&gt;
&lt;li&gt;skill tier;&lt;/li&gt;
&lt;li&gt;tournament bracket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, replace one global board with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;season-12:region-na
season-12:region-eu
season-12:region-apac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distributes writes, but global reads now require a merge. The application&lt;br&gt;
must fetch candidates from each partition and combine them with the same score&lt;br&gt;
and tie-break semantics.&lt;/p&gt;

&lt;p&gt;That is not a free optimization. Partition only when measurement shows that&lt;br&gt;
the exceptional leaderboard needs it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Multi-leaderboard writes cross a different boundary
&lt;/h2&gt;

&lt;p&gt;Sometimes one player update must fan out to many independent boards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global
region-eu
mode-solo
season-12
clan-42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those boards intentionally occupy different slots. They cannot be one atomic&lt;br&gt;
Redis script.&lt;/p&gt;

&lt;p&gt;Podium handles this at the API layer with an &lt;code&gt;errgroup&lt;/code&gt; and caps concurrency at&lt;br&gt;
32 workers per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupCtx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errgroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;leaderboardID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;leaderboardIDs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;leaderboardID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;leaderboardID&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updateOneLeaderboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groupCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;leaderboardID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cap prevents one request containing hundreds of boards from creating&lt;br&gt;
unbounded goroutines and flooding Redis.&lt;/p&gt;

&lt;p&gt;It also exposes an important semantic difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A multi-member write to one leaderboard can be atomic inside one slot.&lt;/li&gt;
&lt;li&gt;A one-member write to many leaderboards is concurrent, but not globally
atomic across slots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your product requires all-or-nothing state across leaderboards, you need a&lt;br&gt;
higher-level workflow with idempotency, durable intent, retries, and&lt;br&gt;
compensation. Redis Cluster cannot manufacture a cross-shard transaction for&lt;br&gt;
you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Test the topology you claim to support
&lt;/h2&gt;

&lt;p&gt;A mocked Redis client cannot prove that keys share slots. A standalone Redis&lt;br&gt;
instance cannot produce &lt;code&gt;CROSSSLOT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Podium's CI runs integration tests against a real cluster with at least three&lt;br&gt;
primaries and verifies coverage of all 16,384 slots. The same tie-break,&lt;br&gt;
expiration, and deletion paths then run through the cluster client.&lt;/p&gt;

&lt;p&gt;The topology check lives in&lt;br&gt;
&lt;a href="https://github.com/TeneficGames/podium/blob/main/leaderboard/redis_cluster_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;redis_cluster_test.go&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This catches a class of failure that unit tests naturally miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;malformed cluster creation;&lt;/li&gt;
&lt;li&gt;incomplete slot coverage;&lt;/li&gt;
&lt;li&gt;accidental cross-slot key changes;&lt;/li&gt;
&lt;li&gt;code paths that work only with the standalone client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Redis Cluster is in your production architecture diagram, a real cluster&lt;br&gt;
belongs somewhere in your test matrix.&lt;/p&gt;
&lt;h2&gt;
  
  
  A more honest scaling checklist
&lt;/h2&gt;

&lt;p&gt;Before calling a Redis-backed system "horizontally scalable," ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is the unit of sharding: tenant, leaderboard, member, or key?&lt;/li&gt;
&lt;li&gt;Which keys must be co-located for atomic operations?&lt;/li&gt;
&lt;li&gt;What is the hottest possible unit?&lt;/li&gt;
&lt;li&gt;Can that unit fit on one primary?&lt;/li&gt;
&lt;li&gt;Which workflows cross slots?&lt;/li&gt;
&lt;li&gt;Are those workflows atomic, eventually consistent, or compensating?&lt;/li&gt;
&lt;li&gt;Does CI exercise a real cluster topology?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For Podium, the unit is the leaderboard.&lt;/p&gt;

&lt;p&gt;That choice preserves simple, atomic ranking operations and distributes fleets&lt;br&gt;
of independent boards well. It also means one exceptional board has a&lt;br&gt;
single-node ceiling.&lt;/p&gt;

&lt;p&gt;Architecture gets easier to reason about when we state both halves.&lt;/p&gt;

&lt;p&gt;If you are designing with Redis Cluster, what is your real unit of sharding?&lt;br&gt;
The answer is often more useful than the number of nodes. Share your design or&lt;br&gt;
questions in the comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Explore Podium on GitHub&lt;/a&gt;
&lt;/p&gt;

</description>
      <category>redis</category>
      <category>architecture</category>
      <category>scalability</category>
      <category>backend</category>
    </item>
    <item>
      <title>Your Redis Leaderboard Is Probably Breaking Ties Wrong</title>
      <dc:creator>Trung Duong</dc:creator>
      <pubDate>Fri, 31 Jul 2026 06:41:29 +0000</pubDate>
      <link>https://dev.to/trungdlp/your-redis-leaderboard-is-probably-breaking-ties-wrong-39k4</link>
      <guid>https://dev.to/trungdlp/your-redis-leaderboard-is-probably-breaking-ties-wrong-39k4</guid>
      <description>&lt;p&gt;A leaderboard looks like a one-command problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ZADD weekly 100 alice
ZADD weekly 100 bob
ZREVRANGE weekly 0 -1 WITHSCORES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;While building&lt;br&gt;
&lt;a href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;Podium&lt;/a&gt;, an open-source Redis-backed&lt;br&gt;
leaderboard service, we discovered that the difficult part begins when two&lt;br&gt;
players have the same score. We are sharing the design because this edge case&lt;br&gt;
can silently turn player IDs into ranking rules.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/TeneficGames" rel="noopener noreferrer"&gt;
        TeneficGames
      &lt;/a&gt; / &lt;a href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;
        podium
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      High-performance, Redis-backed leaderboards for games and competitive applications.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Podium&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/TeneficGames/podium/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/TeneficGames/podium" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5c8cf6e20a38446e700169d3b28728ecf687ce62ee8e8501850b6c69c562cad0/68747470733a2f2f636f6465636f762e696f2f67682f54656e6566696347616d65732f706f6469756d2f6272616e63682f6d61696e2f67726170682f62616467652e737667" alt="codecov"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/TeneficGames/podium/docs/assets/podium-hero.webp"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FTeneficGames%2Fpodium%2FHEAD%2Fdocs%2Fassets%2Fpodium-hero.webp" alt="Podium high-performance distributed leaderboard"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;High-performance, Redis-backed leaderboards for games and competitive
applications.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Podium provides ready-to-run HTTP and gRPC APIs for scores, ranks, seasons, and
player-relative views. It is designed for backend teams operating large fleets
of independent leaderboards without provisioning each leaderboard in advance.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fair, deterministic ordering when scores are equal.&lt;/li&gt;
&lt;li&gt;Single and bulk score updates, including multi-leaderboard fan-out.&lt;/li&gt;
&lt;li&gt;Standalone Redis and real Redis Cluster integration coverage.&lt;/li&gt;
&lt;li&gt;Deploy one multi-architecture OCI image with Docker, containerd, Kubernetes
or another OCI-compatible runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium#quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt; · &lt;a href="https://github.com/TeneficGames/podium#performance" rel="noopener noreferrer"&gt;Performance&lt;/a&gt; ·
&lt;a href="https://github.com/TeneficGames/podium/docs/API.md" rel="noopener noreferrer"&gt;API&lt;/a&gt; · &lt;a href="https://github.com/TeneficGames/podium/docs/overview.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; ·
&lt;a href="https://github.com/TeneficGames/podium/charts/podium/README.md" rel="noopener noreferrer"&gt;Helm chart&lt;/a&gt; ·
&lt;a href="https://hub.docker.com/r/trungdlp/podium" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://github.com/orgs/TeneficGames/packages/container/package/podium" rel="noopener noreferrer"&gt;GHCR&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quickstart&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Start Redis 8.2 and the latest stable Podium image:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker network create podium
docker run --detach --name podium-redis --network podium redis:8.2-alpine

docker run --detach --rm --name podium \
  --network podium \
  --publish 8880:8880 \
  --publish 8881:8881 \
  --env PODIUM_REDIS_HOST=podium-redis \
  --env PODIUM_REDIS_PORT=6379 \
  trungdlp/podium:latest start&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Verify the service:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl http://localhost:8880/healthcheck&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;WORKING
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Submit two equal scores:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl --request&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/TeneficGames/podium" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Both players have 100 points. Alice arrived first, so most game designers would&lt;br&gt;
expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. alice: 100
2. bob:   100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that is not what the data model says.&lt;/p&gt;

&lt;p&gt;Redis sorted sets order members with equal scores lexicographically. With a&lt;br&gt;
reverse range, that secondary ordering is reversed too. Your "fair" tie may&lt;br&gt;
therefore be decided by a player ID.&lt;/p&gt;

&lt;p&gt;That is deterministic, but it is not meaningful.&lt;/p&gt;

&lt;p&gt;We wanted a rule players could understand:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When scores are equal, the player who reached the current score first ranks&lt;br&gt;
higher.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It sounds simple. Implementing it correctly under concurrent writes was not.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the obvious fixes fail
&lt;/h2&gt;

&lt;p&gt;The first idea is usually a timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tie_break = current_time_in_milliseconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates three problems.&lt;/p&gt;

&lt;p&gt;First, two requests can land in the same millisecond. More precision makes that&lt;br&gt;
less likely, but never makes it impossible.&lt;/p&gt;

&lt;p&gt;Second, application clocks are not a reliable global ordering mechanism. If&lt;br&gt;
multiple API replicas write to the same Redis instance, clock skew can reorder&lt;br&gt;
arrivals.&lt;/p&gt;

&lt;p&gt;Third, the score update and timestamp allocation are separate operations unless&lt;br&gt;
you add a transaction or script. A process can fail between them and leave&lt;br&gt;
partial state.&lt;/p&gt;

&lt;p&gt;Another tempting approach is to pack the score and timestamp into one floating&lt;br&gt;
point number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composite = score * scale + tie_break
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis sorted-set scores are IEEE 754 doubles. Integers are represented exactly&lt;br&gt;
only up to &lt;code&gt;2^53 - 1&lt;/code&gt;. Packing two independently growing values into that space&lt;br&gt;
quietly reduces the safe range of both. It can work when your limits are small&lt;br&gt;
and explicit, but it is a dangerous default for a general leaderboard service.&lt;/p&gt;

&lt;p&gt;We needed an ordering token that was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unique under concurrency;&lt;/li&gt;
&lt;li&gt;independent of wall clocks;&lt;/li&gt;
&lt;li&gt;assigned only when a player reaches a new score;&lt;/li&gt;
&lt;li&gt;committed atomically with every affected index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis itself was the right place to allocate it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use a Redis sequence, not time
&lt;/h2&gt;

&lt;p&gt;Podium stores a per-leaderboard sequence initialized to the largest signed&lt;br&gt;
64-bit integer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9223372036854775807
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a member reaches a new score, an atomic Lua script decrements that value&lt;br&gt;
and formats it as a fixed-width 19-digit prefix.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DECR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sequence_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sequence_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;left_pad_to_19_digits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;public_member_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first arrival receives a larger token than the second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alice -&amp;gt; 9223372036854775806alice
bob   -&amp;gt; 9223372036854775805bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a descending leaderboard, Redis's reverse lexicographic ordering now does&lt;br&gt;
exactly what we need. At equal scores, Alice's larger internal token comes&lt;br&gt;
first.&lt;/p&gt;

&lt;p&gt;The public ID has not changed. The encoded value is an internal member name,&lt;br&gt;
and a Redis hash maps each public ID to its current token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public member ID -&amp;gt; current internal token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mapping is essential when a score changes. We remove the old internal&lt;br&gt;
member, allocate a new sequence, add the replacement, and update the mapping in&lt;br&gt;
one script.&lt;/p&gt;

&lt;p&gt;You can see the production implementation in&lt;br&gt;
&lt;a href="https://github.com/TeneficGames/podium/blob/main/leaderboard/database/redis/tiebreak.go" rel="noopener noreferrer"&gt;&lt;code&gt;tiebreak.go&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Idempotency is part of fairness
&lt;/h2&gt;

&lt;p&gt;Suppose Alice reaches 100 first and Bob reaches 100 second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. alice: 100
2. bob:   100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alice's client retries the same request because the network response was lost.&lt;br&gt;
Should she get a new arrival token?&lt;/p&gt;

&lt;p&gt;No. A retry must not change rank.&lt;/p&gt;

&lt;p&gt;Before allocating a sequence, the Lua script looks up Alice's current internal&lt;br&gt;
token and reads its score. If the submitted score is unchanged, it preserves&lt;br&gt;
the token.&lt;/p&gt;

&lt;p&gt;That gives the rule precise semantics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Submitting the same score again keeps the current position.&lt;/li&gt;
&lt;li&gt;Leaving a score and returning later creates a new arrival.&lt;/li&gt;
&lt;li&gt;Removing and re-adding a member creates a new arrival.&lt;/li&gt;
&lt;li&gt;Incrementing by zero internally preserves the arrival token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not edge cases to decide after launch. They are part of the product&lt;br&gt;
contract.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ascending leaderboards contain a trap
&lt;/h2&gt;

&lt;p&gt;Descending leaderboards are common: more points is better.&lt;/p&gt;

&lt;p&gt;But some competitions are ascending: less time, fewer moves, or fewer strokes&lt;br&gt;
is better.&lt;/p&gt;

&lt;p&gt;Redis uses normal lexicographic order for &lt;code&gt;ZRANK&lt;/code&gt; and reverse lexicographic&lt;br&gt;
order for &lt;code&gt;ZREVRANK&lt;/code&gt;. One encoded member cannot put the earlier arrival first in&lt;br&gt;
both directions.&lt;/p&gt;

&lt;p&gt;Podium therefore maintains two score indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:scores      -&amp;gt; descending tie order
:scores-asc  -&amp;gt; ascending tie order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The descending token uses the decreasing sequence directly. The ascending&lt;br&gt;
token inverts each sequence digit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9 - digit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An earlier, larger sequence becomes a lexicographically smaller prefix in the&lt;br&gt;
ascending index. The same player-facing rule now holds in both directions.&lt;/p&gt;

&lt;p&gt;This second index costs memory, but it avoids conditional definitions of&lt;br&gt;
"first" that change with leaderboard direction.&lt;/p&gt;
&lt;h2&gt;
  
  
  Atomicity is the real feature
&lt;/h2&gt;

&lt;p&gt;One score update can touch four pieces of state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;descending sorted set
ascending sorted set
public-ID-to-token hash
sequence counter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doing that with four client round trips creates four places for races and&lt;br&gt;
partial failure.&lt;/p&gt;

&lt;p&gt;Podium runs the operation as one Redis Lua script. Redis executes scripts&lt;br&gt;
atomically, so concurrent arrivals receive distinct sequence values and readers&lt;br&gt;
never observe a half-updated member.&lt;/p&gt;

&lt;p&gt;The script also returns the resulting rank. The caller does not need a separate&lt;br&gt;
"write, then read rank" round trip that another request could interleave with.&lt;/p&gt;

&lt;p&gt;The concurrency tests launch 128 simultaneous arrivals at the same score and&lt;br&gt;
verify that every member receives a unique rank. Separate tests cover retries,&lt;br&gt;
score changes, removal, re-entry, ascending order, and sequence exhaustion.&lt;/p&gt;

&lt;p&gt;The tests are worth reading alongside the implementation:&lt;br&gt;
&lt;a href="https://github.com/TeneficGames/podium/blob/main/leaderboard/database/redis/tiebreak_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;tiebreak_test.go&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Correctness is not free
&lt;/h2&gt;

&lt;p&gt;The plain sorted-set design is smaller and faster.&lt;/p&gt;

&lt;p&gt;In our direct Redis benchmarks, deterministic ordering added approximately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Latency overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Insert and return rank&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change score and return rank&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submit the same score&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read one rank&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read the top 50&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Supporting both directions used about 287 Redis bytes per member, compared with&lt;br&gt;
99 bytes for the plain baseline in the measured setup.&lt;/p&gt;

&lt;p&gt;Those numbers are not universal capacity promises. They came from local&lt;br&gt;
benchmarks with a specific Redis version, machine, and dataset. They do make&lt;br&gt;
the tradeoff visible: we spend memory and a modest amount of latency to replace&lt;br&gt;
an arbitrary player-ID tie-break with a stable product rule.&lt;/p&gt;

&lt;p&gt;For a game where tied players care who arrived first, that is an easy trade to&lt;br&gt;
explain.&lt;/p&gt;
&lt;h2&gt;
  
  
  The deeper lesson
&lt;/h2&gt;

&lt;p&gt;Redis gives you a total order. That does not mean it gives you the &lt;em&gt;right&lt;/em&gt;&lt;br&gt;
order.&lt;/p&gt;

&lt;p&gt;Before shipping a leaderboard, write down the answers to these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What happens when scores tie?&lt;/li&gt;
&lt;li&gt;Does retrying the same write change position?&lt;/li&gt;
&lt;li&gt;What happens after a player leaves and returns to a score?&lt;/li&gt;
&lt;li&gt;Can concurrent writes receive the same tie-break value?&lt;/li&gt;
&lt;li&gt;Does the rule behave identically for ascending and descending boards?&lt;/li&gt;
&lt;li&gt;Are the score and tie-break committed atomically?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answer to the first question is "Redis handles it," your member IDs may&lt;br&gt;
already be deciding matches.&lt;/p&gt;

&lt;p&gt;Podium exposes this behavior through HTTP and gRPC. I would love to hear how&lt;br&gt;
your system resolves equal scores and which tie rule your players consider&lt;br&gt;
fair. Share your approach or questions in the comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TeneficGames/podium" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Explore Podium on GitHub&lt;/a&gt;
&lt;/p&gt;

</description>
      <category>redis</category>
      <category>go</category>
      <category>gamedev</category>
      <category>backend</category>
    </item>
    <item>
      <title>😴 Go Code You Can Trust: Sleep Well After You Commit</title>
      <dc:creator>Trung Duong</dc:creator>
      <pubDate>Thu, 13 Mar 2025 03:11:52 +0000</pubDate>
      <link>https://dev.to/trungdlp/go-code-you-can-trust-sleep-well-after-you-commit-440n</link>
      <guid>https://dev.to/trungdlp/go-code-you-can-trust-sleep-well-after-you-commit-440n</guid>
      <description>&lt;p&gt;It's Friday afternoon, 4:45 PM. My teammates are already discussing weekend plans in Slack. I'm finishing up a critical piece of our payment processing service. The code is done, tests pass, and I'm about to commit it before heading into the weekend. My finger hovers over the enter key for a moment...&lt;/p&gt;

&lt;p&gt;But there's no hesitation, no worry. I commit the code, close my laptop, and join the weekend conversation without a second thought about my code failing in production.&lt;/p&gt;

&lt;p&gt;This wasn't always the case. Ten years ago, I'd be checking my phone all weekend, worried about 3 AM calls from our on-call engineer.&lt;/p&gt;

&lt;p&gt;What changed? Let me tell you my story of how I learned to write Go code I could trust completely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzn393n409zl98ytlzccj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzn393n409zl98ytlzccj.png" alt="Golang sleep" width="512" height="512"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Image source: Medium&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Cost of Uncertain Code
&lt;/h2&gt;

&lt;p&gt;My journey with Go started in 2021 when I was working at a rapidly growing startup. We were moving from a monolithic Java application to microservices, and Go seemed like the perfect fit: fast, simple, and with built-in concurrency.&lt;/p&gt;

&lt;p&gt;But my early Go code looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ProcessPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paymentID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Get payment details&lt;/span&gt;
    &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// Which error? What happened?&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Process the payment&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// Again, what went wrong?&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Update payment status&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"processed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// Did the payment go through? Is it in a bad state?&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functionally, this code worked. But it haunted me at night because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Error handling was minimal and provided no context&lt;/li&gt;
&lt;li&gt;There was no logging for debugging&lt;/li&gt;
&lt;li&gt;No consideration for partial failures&lt;/li&gt;
&lt;li&gt;No validation of inputs&lt;/li&gt;
&lt;li&gt;No testing for edge cases&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result? Support tickets on Saturday mornings. Mysterious payment failures. Hours spent on debugging sessions. And the constant anxiety that something might be failing silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Path to Trustworthy Code
&lt;/h2&gt;

&lt;p&gt;Everything changed when I joined a team at WF. My mentor there had a simple philosophy: "Write every line of code as if you'll be on vacation when it runs in production."&lt;/p&gt;

&lt;p&gt;This mindset shift transformed how I approached Go development. Here's what I learned, and what now lets me sleep well after committing code:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Errors Are Your Friends, Not Exceptions
&lt;/h3&gt;

&lt;p&gt;Go's error handling is verbose but powerful when used correctly. The secret is to treat errors as valuable information carriers, not just failure signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ProcessPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paymentID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Get payment details&lt;/span&gt;
    &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to retrieve payment %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Validate before proceeding&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validatePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payment validation failed for %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Process the payment with context&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Log additional details for debugging&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"payment_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"currency"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Payment processing failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gateway failed to process payment %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Success path is clearly logged too&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payment_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paymentID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Payment processed successfully"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference? When something goes wrong, I have rich context. The error messages tell a story, making debugging faster and easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tests That Give You Confidence
&lt;/h3&gt;

&lt;p&gt;My earlier self wrote tests that proved the code worked in the happy path. My current self writes tests that prove the code won't break in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestProcessPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Happy path test&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"successful payment processing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Setup and assertions&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c"&gt;// What happens when things go wrong&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"database unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Simulate DB failure&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payment gateway timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Simulate slow payment gateway&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid payment data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Test validation logic&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"partial failure - payment processed but status update fails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Test recovery mechanisms&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A comprehensive test suite is like insurance. It doesn't prevent all problems, but it significantly reduces the likelihood of common failures reaching production.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Graceful Degradation
&lt;/h3&gt;

&lt;p&gt;One key insight: not all failures are equal. The best Go code doesn't just handle errors; it gracefully degrades functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetUserRecommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Recommendation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Try to get personalized recommendations&lt;/span&gt;
    &lt;span class="n"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;recommendationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPersonalized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Log the error&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to get personalized recommendations, falling back to popular items"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c"&gt;// Fall back to popular recommendations&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;recommendationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPopular&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern means your code tries its best to provide value, even when some components fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Monitoring and Observability Built-In
&lt;/h3&gt;

&lt;p&gt;Code I can trust doesn't just work well; it tells me how it's working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ProcessOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Start timing the operation&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Use defer to ensure metrics are always recorded&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ObserveOrderProcessingTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IncrementOrdersProcessed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="c"&gt;// Trace this operation for distributed tracing&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartSpanFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"process_order"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Add helpful information to the trace&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Process the order with the traced context&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;orderProcessor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Record error in metrics and trace&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IncrementOrderErrors&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogKV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error.message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"processing order %s failed: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When your code has built-in monitoring, you don't need to wonder if it's working correctly in production. You &lt;em&gt;know&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-Life Example: The 3 AM Bug That Never Called
&lt;/h2&gt;

&lt;p&gt;Last year, we deployed a new feature to our payment system right before a long holiday weekend. The old me would have been anxious the entire time, but I wasn't worried at all.&lt;/p&gt;

&lt;p&gt;Sure enough, something unexpected happened: a third-party API we depended on changed their response format. But instead of a production outage, here's what happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our validation caught the changed format and returned a clear error&lt;/li&gt;
&lt;li&gt;The circuit breaker we implemented prevented cascading failures&lt;/li&gt;
&lt;li&gt;The system fell back to a secondary processing method&lt;/li&gt;
&lt;li&gt;Our monitoring alerted the on-call engineer with the exact issue&lt;/li&gt;
&lt;li&gt;Detailed logs showed exactly where and how the format had changed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The on-call engineer fixed it with a simple config change—no emergency, no all-hands debugging session.&lt;/p&gt;

&lt;p&gt;The best part? I only found out about this when I read the incident report on Tuesday morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Write Go Code You Can Trust
&lt;/h2&gt;

&lt;p&gt;After a decade of writing Go, here's my checklist for code I can trust enough to disconnect completely from work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Handle errors with context&lt;/strong&gt;: Wrap errors, add information, make debugging easy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test failure modes&lt;/strong&gt;: Don't just test success cases; test what happens when things fail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build in graceful degradation&lt;/strong&gt;: Design systems that bend rather than break&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it observable&lt;/strong&gt;: Logging, metrics, and tracing are not afterthoughts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate early and thoroughly&lt;/strong&gt;: Catch bad inputs before they cause damage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document assumptions&lt;/strong&gt;: Clear documentation helps future you and your teammates&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The difference between Go code that keeps you up at night and Go code you can trust isn't about clever algorithms or advanced features. It's about care, attention to detail, and a mindset that prepares for the unexpected.&lt;/p&gt;

&lt;p&gt;When I interview Go developers now, I don't just look at how well they can write code that works. I look at how they handle the edge cases, how they think about failures, and whether their code would let them enjoy their weekends without worry.&lt;/p&gt;

&lt;p&gt;Because in the end, the best code isn't just functionally correct—it's trustworthy enough that you can commit it on Friday afternoon and genuinely disconnect until Monday morning.&lt;/p&gt;

&lt;p&gt;And for me, that peace of mind is worth every extra line of error handling, every additional test case, and every minute spent making my code more robust.&lt;/p&gt;

&lt;p&gt;So next time you're writing Go code, ask yourself: "Would I sleep well tonight if this ran in production after I left?" If the answer isn't a confident "yes," you have more work to do.&lt;/p&gt;

&lt;p&gt;Your future self—possibly on a beach somewhere without laptop access—will thank you.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>go</category>
      <category>coding</category>
      <category>webdev</category>
    </item>
    <item>
      <title>ByteDance/Sonic: A Lightning-Fast JSON Library for Go</title>
      <dc:creator>Trung Duong</dc:creator>
      <pubDate>Tue, 04 Feb 2025 04:20:16 +0000</pubDate>
      <link>https://dev.to/trungdlp/bytedancesonic-a-lightning-fast-json-library-for-go-1931</link>
      <guid>https://dev.to/trungdlp/bytedancesonic-a-lightning-fast-json-library-for-go-1931</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"In the world of microservices, every millisecond counts. See how TikTok's engineering team revolutionized JSON processing in Go."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbb5r59ha04xxus2b36qx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbb5r59ha04xxus2b36qx.png" alt="JSON" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you ever deployed a Go service and watched your CPU spike when processing thousands of JSON requests? You're not alone. TikTok's engineers faced this exact challenge at a massive scale - billions of requests per day. Their solution? They created Sonic, a JSON library that's changing the game for Go developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JSON Problem Every Go Developer Knows
&lt;/h2&gt;

&lt;p&gt;If you're a Go developer, you've probably used the standard &lt;code&gt;encoding/json&lt;/code&gt; package. It works, but let's be real - it's not the fastest kid on the block. If you're building modern web services or APIs, you're working with JSON every day - from REST APIs to configuration files. When TikTok's engineers found their services processing millions of JSON requests per second, even small performance improvements in JSON handling could make a huge difference in server costs and user experience.&lt;/p&gt;

&lt;p&gt;Let's look at a common scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Your typical JSON processing with standard library&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"name"`&lt;/span&gt;
    &lt;span class="n"&gt;Posts&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt; &lt;span class="s"&gt;`json:"posts"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Processing this for millions of requests...&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At small scale, this works fine. But when you're handling TikTok-scale traffic, those milliseconds add up to significant server costs and latency. That's exactly why ByteDance's team decided to tackle this challenge head-on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Speed Champion: Meet Sonic
&lt;/h2&gt;

&lt;p&gt;Let's look at some real numbers first. When working with a medium-sized JSON file (about 13KB):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Processing user profile with posts and metadata&lt;/span&gt;
&lt;span class="n"&gt;Standard&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;106&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;322&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nanoseconds&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Sonic&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;393&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;
&lt;span class="n"&gt;Memory&lt;/span&gt; &lt;span class="n"&gt;Usage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;136&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="m"&gt;789&lt;/span&gt; &lt;span class="n"&gt;allocations&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Sonic&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="m"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;965&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="n"&gt;allocations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/bytedance/sonic/blob/main/testdata/small.go" rel="noopener noreferrer"&gt;Small&lt;/a&gt; (400B, 11 keys, 3 layers)
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0sl259re9u93h70w818.png" alt="small benchmarks" width="799" height="303"&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bytedance/sonic/blob/main/testdata/twitter.json" rel="noopener noreferrer"&gt;Large&lt;/a&gt; (635KB, 10000+ key, 6 layers)
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfvqo7bwgd0ddk2aiqp1.png" alt="large benchmarks" width="799" height="289"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://github.com/bytedance/sonic/blob/main/scripts/bench.sh" rel="noopener noreferrer"&gt;bench.sh&lt;/a&gt; for benchmark codes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Secret Sauce: Four Simple Tricks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Just-In-Time Compilation (JIT)
&lt;/h3&gt;

&lt;p&gt;Imagine you're a chef. Instead of following a generic recipe every time, you create a special, optimized recipe for each specific dish you make frequently. That's what JIT does!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regular Go JSON library: Uses the same generic code for all JSON&lt;/li&gt;
&lt;li&gt;Sonic: Creates specialized code paths for your specific JSON structures&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. SIMD: Doing More Work at Once
&lt;/h3&gt;

&lt;p&gt;Think of SIMD like having multiple hands to do a task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regular way: Sort cards one by one&lt;/li&gt;
&lt;li&gt;SIMD way: Sort multiple cards at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sonic uses these "multiple hands" (SIMD instructions) to process JSON data in parallel, making everything faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Smart Memory Usage
&lt;/h3&gt;

&lt;p&gt;Here's a clever trick Sonic uses: When it finds a string in your JSON that doesn't have any special characters, it doesn't make a copy. Instead, it just points to the original string. It's like giving directions instead of drawing a new map!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Example JSON&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"John"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"city"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"New York"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Standard library: Makes copies of "John" and "New York"&lt;/span&gt;
&lt;span class="c"&gt;// Sonic: Just references these strings if they're simple&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Optional Features = Better Speed
&lt;/h3&gt;

&lt;p&gt;Sonic makes some smart choices about what features to make optional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't sort map keys by default (saves 10% processing time)&lt;/li&gt;
&lt;li&gt;Doesn't escape HTML by default (saves 15% processing time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can still turn these features on if you need them, but by making them optional, Sonic stays fast for most common uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;The design is easy to implement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Aiming at the function-call overhead cost by the codec dynamic-assembly, &lt;strong&gt;&lt;code&gt;JIT&lt;/code&gt; tech is used to assemble opcodes (asm) corresponding to the schema at runtime&lt;/strong&gt;, which is finally cached into the off-heap memory in the form of Golang functions.&lt;/li&gt;
&lt;li&gt;For practical scenarios where big data and small data coexist, we &lt;strong&gt;use pre-conditional judgment&lt;/strong&gt; (string size, floating precision, etc.) &lt;strong&gt;to combine &lt;code&gt;SIMD&lt;/code&gt; with scalar instructions&lt;/strong&gt; to achieve the best adaptation.&lt;/li&gt;
&lt;li&gt;As for insufficiency in compiling optimization of go language, we decided to &lt;strong&gt;use &lt;code&gt;C/Clang&lt;/code&gt; to write and compile core computational functions&lt;/strong&gt;, and &lt;strong&gt;developed a set of &lt;a href="https://github.com/chenzhuoyu/asm2asm" rel="noopener noreferrer"&gt;asm2asm&lt;/a&gt; tools to translate the fully optimized x86 assembly into plan9&lt;/strong&gt; and finally load it into Golang runtime.&lt;/li&gt;
&lt;li&gt;Giving the big speed gap between parsing and skipping, the &lt;strong&gt;&lt;code&gt;lazy-load&lt;/code&gt; mechanism&lt;/strong&gt; is certainly used in our AST parser, but in &lt;strong&gt;a more adaptive and efficient way to reduce the overhead of multiple-key queries&lt;/strong&gt;.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2eu09g87hnywu1td9rwx.png" alt="design" width="800" height="506"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In detail,  we conducted some further optimization:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Since the native-asm functions cannot be inlined in Golang, we found that its cost even exceeded the improvement brought by the optimization of the C compiler. So we reimplemented a set of lightweight function-calls in JIT:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Global-function-table + static offset&lt;/code&gt; for calling instruction&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pass parameters using registers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sync.Map&lt;/code&gt; was used to cache the codecs at first, but for our &lt;strong&gt;quasi-static&lt;/strong&gt; (read far more than write), &lt;strong&gt;fewer elements&lt;/strong&gt; (usually no more than a few dozen) scenarios, its performance is not optimal, so we reimplement a high-performance and concurrent-safe cache with &lt;code&gt;open-addressing-hash + RCU&lt;/code&gt; tech.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/bytedance/sonic/blob/main/docs/INTRODUCTION.md#design" rel="noopener noreferrer"&gt;From Sonic Design&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Benefits
&lt;/h2&gt;

&lt;p&gt;Let's talk numbers that matter in the real world:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Memory Usage (for medium JSON):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard Go: Uses about 49KB of memory with 789 allocations&lt;/li&gt;
&lt;li&gt;Sonic: Uses only about 12KB with just 4 allocations&lt;/li&gt;
&lt;li&gt;That's huge when you're processing millions of requests!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Practical Impact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster API responses&lt;/li&gt;
&lt;li&gt;Lower server costs&lt;/li&gt;
&lt;li&gt;Better user experience&lt;/li&gt;
&lt;li&gt;More requests handled per server&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Should You Switch to Sonic?
&lt;/h2&gt;

&lt;p&gt;Sonic might be great for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You process lots of JSON data&lt;/li&gt;
&lt;li&gt;Performance is important for your application&lt;/li&gt;
&lt;li&gt;You're working on AMD64 or ARM64 processors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It requires Go version 1.17 or higher&lt;/li&gt;
&lt;li&gt;It works on Linux, MacOS, and Windows&lt;/li&gt;
&lt;li&gt;Some features (like HTML escaping) need to be explicitly enabled if you need them&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Start Example
&lt;/h2&gt;

&lt;p&gt;Here's how simple it is to use Sonic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/bytedance/sonic"&lt;/span&gt;

&lt;span class="c"&gt;// Encoding&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sonic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Decoding&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sonic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Sonic shows us that even with something as common as JSON processing, there's still room for impressive improvements. By using modern CPU features (SIMD), smart compilation (JIT), and thoughtful design choices, it achieves remarkable performance gains over the standard library.&lt;/p&gt;

&lt;p&gt;Remember: In software development, it's not just about making things work - sometimes, making them work faster can open up new possibilities for what your applications can achieve!&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;Want to dive deeper? Here are some great resources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/bytedance/sonic" rel="noopener noreferrer"&gt;Sonic GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bytedance/sonic/blob/main/docs/INTRODUCTION.md" rel="noopener noreferrer"&gt;Technical Introduction Document&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bytedance/sonic/blob/main/docs/INTRODUCTION.md#benchmarks" rel="noopener noreferrer"&gt;Sonic Benchmark Details&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_data" rel="noopener noreferrer"&gt;SIMD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Just-in-time_compilation" rel="noopener noreferrer"&gt;JIT compilation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://victoriametrics.com/blog/go-sync-map/" rel="noopener noreferrer"&gt;sync.Map&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;What do you think about Sonic? Have you tried using it in your Go projects? Let me know in the comments below!&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>json</category>
      <category>restapi</category>
    </item>
  </channel>
</rss>
