[{"data":1,"prerenderedAt":967},["ShallowReactive",2],{"article-building-shortened-cv":3},{"id":4,"title":5,"architecture":6,"body":15,"category":950,"cover":951,"date":952,"description":953,"extension":954,"featured":955,"meta":956,"navigation":957,"path":958,"readingTime":951,"seo":959,"stem":960,"tags":961,"updated":951,"__hash__":966},"articles\u002Farticles\u002Fbuilding-shortened-cv.md","How I Built a URL Shortener That Handled 3,200 Redirects\u002FSecond with Go, Redis, and PostgreSQL",{"image":7,"alt":8,"width":9,"height":10,"caption":11,"video":12,"videoPoster":13,"videoDuration":14},"\u002Farticles\u002Fbuilding-shortened-cv\u002Farchitecture.png","shortened.cv architecture: visitors reach the Go API, which checks the local LRU cache, Redis, and PostgreSQL before returning a 302 redirect. Click events are buffered and batched into Redis Streams, consumed into PostgreSQL, and aggregated into daily analytics rollups.",1448,1086,"Fast redirects through the cache; click analytics processed asynchronously. The benchmark figures shown are local measurements.","\u002Farticles\u002Fbuilding-shortened-cv\u002Farchitecture.mp4","\u002Farticles\u002Fbuilding-shortened-cv\u002Farchitecture-video-poster.webp","0:40",{"type":16,"value":17,"toc":934},"minimark",[18,22,31,34,49,72,79,82,87,95,102,105,118,121,125,128,138,149,152,156,171,182,185,188,192,195,198,204,210,213,220,223,233,237,240,247,250,253,256,270,273,286,289,293,296,303,306,330,334,358,361,449,463,477,493,500,508,513,516,592,595,602,609,670,677,701,705,708,738,751,755,766,773,843,850,861,864,881,885,888,891,894,897,927,930],[19,20,5],"h1",{"id":21},"how-i-built-a-url-shortener-that-handled-3200-redirectssecond-with-go-redis-and-postgresql",[23,24,25,26,30],"p",{},"A URL redirect sounds simple: look up a short code, find its destination, and return an HTTP ",[27,28,29],"code",{},"302",".",[23,32,33],{},"But I wanted to know what happens when thousands of requests arrive every second and the same service also needs to create links, record clicks, and serve analytics. What happens if Redis disappears? Can people still follow their links if PostgreSQL goes down? And how do you keep retries from counting the same click twice?",[23,35,36,37,44,45,30],{},"I built ",[38,39,43],"a",{"href":40,"rel":41},"https:\u002F\u002Fapp.shortened.cv",[42],"nofollow","shortened.cv"," to explore those questions using ",[46,47,48],"strong",{},"Go, Gin, Redis, PostgreSQL, and Nuxt",[23,50,51,52,55,56,59,60,63,64,67,68,71],{},"In one local, 60-second mixed-workload test, it delivered ",[46,53,54],{},"3,200 redirects\u002Fsecond",", alongside ",[46,57,58],{},"100 new links\u002Fsecond"," and ",[46,61,62],{},"10 analytics requests\u002Fsecond",", with ",[46,65,66],{},"7.9 ms redirect p99 latency",". There were no HTTP errors or missed iterations, and all ",[46,69,70],{},"192,000 redirect events"," were reconciled with PostgreSQL after the workers caught up.",[23,73,74,75,78],{},"On the same laptop without container limits, one hot link reached about ",[46,76,77],{},"9,800 redirects\u002Fsecond"," for a minute, and every one of its clicks was stored.",[23,80,81],{},"Both are local benchmarks, not a promise of production capacity. The more interesting story is what I had to build, and what broke along the way.",[83,84,86],"h2",{"id":85},"starting-with-the-requirements","Starting with the requirements",[23,88,89,94],{},[38,90,93],{"href":91,"rel":92},"https:\u002F\u002Fandreyka26.com\u002Furl-shortener-bitly-system-design-with-microsoft-engineer",[42],"Andreyka26's Bitly system-design exercise"," gave me a useful reference workload: 300 million newly created URLs and 3 billion redirects per day, or roughly 3,472 creations and 34,722 redirects per second on average.",[23,96,97,98,101],{},"Those were ",[46,99,100],{},"design targets to explore",", not numbers this implementation has achieved. I wanted to build a small working system first, test it, and find its actual limits.",[23,103,104],{},"At its core, shortened.cv needs to do three things:",[106,107,108,112,115],"ul",{},[109,110,111],"li",{},"Create short links, including links made without signing in.",[109,113,114],{},"Redirect visitors quickly and safely.",[109,116,117],{},"Collect click events and turn them into useful reports.",[23,119,120],{},"I also added custom slugs, anonymous-link claiming, destination previews, and administrative takedowns. But the hardest decisions were behind the API.",[83,122,124],{"id":123},"one-go-service-two-different-paths","One Go service, two different paths",[23,126,127],{},"Redirects need to respond immediately. Analytics can be processed a little later. I designed the system around that difference.",[129,130,136],"pre",{"className":131,"code":133,"language":134,"meta":135},[132],"language-text","                        ┌── Local LRU ── Redis cache ── PostgreSQL\nGET \u002F{slug} ── Go API ─┤        (continue on cache miss)\n                        └── Click buffer ── Redis Stream\n                                                │\n                                                ▼\n                                          Batch consumers\n                                                │\n                                                ▼\n                                           PostgreSQL\n                                                │\n                                                ▼\n                                         Daily rollups\n","text","",[27,137,133],{"__ignoreMap":135},[23,139,140,141,144,145,148],{},"The Go API uses Gin for HTTP handlers, service packages for business rules, ",[27,142,143],{},"sqlc"," for typed database queries, and ",[27,146,147],{},"goose"," for migrations. The frontend is built with Nuxt.",[23,150,151],{},"A redirect should normally need only a cached lookup and a quick enqueue operation. It shouldn't wait for a PostgreSQL analytics insert.",[83,153,155],{"id":154},"generating-unique-short-codes","Generating unique short codes",[23,157,158,159,162,163,166,167,170],{},"Automatically generated links use ",[46,160,161],{},"seven Base62 characters",": lowercase letters, uppercase letters, and digits. That gives ",[27,164,165],{},"62^7",", or about ",[46,168,169],{},"3.52 trillion"," possible codes.",[23,172,173,174,177,178,181],{},"I use Go's ",[27,175,176],{},"crypto\u002Frand"," with rejection sampling to avoid bias when mapping random bytes to those 62 characters. The random generator proposes a slug; ",[46,179,180],{},"PostgreSQL's unique constraint guarantees uniqueness",". If an insertion collides, the API tries another candidate, up to five attempts.",[23,183,184],{},"Soft-deleted links keep their reserved slugs. I didn't want an old shared address to be reassigned to an unrelated destination.",[23,186,187],{},"People can also shorten a link without creating an account. An anonymous link gets a random claim token; the database stores only its hash. If the person signs in later, they can claim the link without losing its existing click history.",[83,189,191],{"id":190},"making-the-hot-path-cheap","Making the hot path cheap",[23,193,194],{},"If every redirect queried PostgreSQL, a viral link would repeat essentially the same database lookup thousands of times.",[23,196,197],{},"I added two cache layers:",[23,199,200,203],{},[46,201,202],{},"L1: a local, size-bounded LRU cache."," Each Go process keeps frequently accessed destinations in memory, with a default capacity of 100,000 links and a 60-second TTL.",[23,205,206,209],{},[46,207,208],{},"L2: Redis."," On a local miss, the service checks the shared Redis cache. Only a miss there requires PostgreSQL. Redis also distributes invalidation messages between API instances.",[23,211,212],{},"Two details matter beyond simply adding a cache:",[23,214,215,216,219],{},"First, concurrent misses for the same slug ",[46,217,218],{},"share one in-flight load"," instead of issuing duplicate database queries. A separate concurrency limit bounds database lookups for different slugs.",[23,221,222],{},"Second, deleting or taking down a link must invalidate cached copies. In an early failure test, a failed Redis revocation allowed an old destination to be loaded into L1 again. I added local revocation tracking and retries to address that case.",[23,224,225,226,228,229,232],{},"Redirects use HTTP ",[27,227,29],{}," rather than ",[27,230,231],{},"301",". A browser can cache a permanent redirect and bypass the shortener on future clicks, which would undermine analytics.",[83,234,236],{"id":235},"recording-clicks-without-blocking-redirects","Recording clicks without blocking redirects",[23,238,239],{},"Every successful redirect can produce an event: timestamp, source, referrer host, device, browser, UTM tags, and available location data. The API also distinguishes bot traffic from human clicks.",[23,241,242,243,246],{},"Writing each event synchronously would make the visitor wait for an additional database operation. Instead, events enter a memory buffer and are sent to a ",[46,244,245],{},"Redis Stream",", normally in batches every 20 milliseconds or when a batch fills.",[23,248,249],{},"Consumers write those events to PostgreSQL in batches. Stream entries are acknowledged and removed only after the database write commits.",[23,251,252],{},"Retries introduce another challenge: a consumer might finish a database write and crash before acknowledging the stream entry. When another consumer replays it, the click must not be counted twice.",[23,254,255],{},"Each click therefore has a unique event ID. The database insert uses the event identity to skip duplicates:",[129,257,261],{"className":258,"code":259,"language":260,"meta":135,"style":135},"language-sql shiki shiki-themes github-light github-dark github-dark","ON CONFLICT (created_at, event_id) DO NOTHING\n","sql",[27,262,263],{"__ignoreMap":135},[264,265,268],"span",{"class":266,"line":267},"line",1,[264,269,259],{},[23,271,272],{},"Only newly inserted, non-bot events increment the corresponding link's human-click counter. That keeps replayed batches from inflating analytics.",[23,274,275,278,279,282,283,30],{},[46,276,277],{},"The trade-off is durability."," The stream relies on Redis append-only persistence (",[27,280,281],{},"appendfsync everysec",") to survive a Redis restart. If Redis is unavailable or the stream exceeds its configured budget, the API falls back to an in-memory PostgreSQL writer. That fallback can lose buffered events on a crash, and its asynchronous-commit setting can trade durability for speed. The normal Redis Stream consumer uses durable database commits, but the complete pipeline is ",[46,284,285],{},"not lossless under every failure",[23,287,288],{},"For privacy, click records don't retain raw IP addresses. Approximate unique-visitor counts use a keyed hash that changes each day.",[83,290,292],{"id":291},"analytics-has-a-different-scaling-problem","Analytics has a different scaling problem",[23,294,295],{},"Storing clicks is only half the job. A dashboard that repeatedly scans millions of raw events can become expensive even when redirects are fast.",[23,297,298,299,302],{},"I partitioned raw clicks by UTC day, keep separate per-link counters, and roll closed days into aggregate tables. The default raw-event retention period is ",[46,300,301],{},"30 days","; daily summaries remain available for longer reports.",[23,304,305],{},"One subtle issue showed up during recovery testing: what if a delayed click belongs to a day that has already been aggregated? A database trigger marks that day as stale so maintenance can recompute the rollup.",[23,307,308,309,312,313,316,317,320,321,59,325,329],{},"In a separate local reporting test with ",[46,310,311],{},"three million seeded historical clicks",", the API completed roughly ",[46,314,315],{},"167 analytics requests\u002Fsecond alongside 1,000 redirects\u002Fsecond"," for 60 seconds. Report p95 was ",[46,318,319],{},"16.5 ms",". The ",[38,322,324],{"href":323},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Freporting-results.json","reporting results",[38,326,328],{"href":327},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Freporting-seed.json","seed details"," record the measurements and dataset size.",[83,331,333],{"id":332},"what-the-local-benchmarks-actually-showed","What the local benchmarks actually showed",[23,335,336,337,340,341,344,345,348,349,352,353,357],{},"I used ",[46,338,339],{},"Grafana k6"," with a disposable Docker environment on an Intel Core i7-8706G laptop with 16 GB RAM. The API had a ",[46,342,343],{},"2-CPU \u002F 512 MiB"," limit, PostgreSQL ",[46,346,347],{},"2 CPUs \u002F 1 GiB",", and Redis ",[46,350,351],{},"0.5 CPU \u002F 256 MiB",". k6 ran on the same host, over direct HTTP without a reverse proxy or TLS. The ",[38,354,356],{"href":355},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Flocal-environment.json","environment details"," record the commit, container limits, and settings.",[23,359,360],{},"The starting redirect dataset contained 1,000 links, which fit in the local cache. So these numbers are useful for warm-link performance, but they don't establish performance across millions of cold links.",[362,363,364,384],"table",{},[365,366,367],"thead",{},[368,369,370,374,378,381],"tr",{},[371,372,373],"th",{},"Offered workload",[371,375,377],{"align":376},"right","Duration",[371,379,380],{"align":376},"Redirect p99",[371,382,383],{},"Outcome",[385,386,387,402,423,437],"tbody",{},[368,388,389,393,396,399],{},[390,391,392],"td",{},"1,000 redirects\u002Fs + 50 creates\u002Fs + 10 reports\u002Fs",[390,394,395],{"align":376},"120 s",[390,397,398],{"align":376},"1.3 ms",[390,400,401],{},"Completed",[368,403,404,409,414,419],{},[390,405,406],{},[46,407,408],{},"3,200 redirects\u002Fs + 100 creates\u002Fs + 10 reports\u002Fs",[390,410,411],{"align":376},[46,412,413],{},"60 s",[390,415,416],{"align":376},[46,417,418],{},"7.9 ms",[390,420,421],{},[46,422,401],{},[368,424,425,428,431,434],{},[390,426,427],{},"5,000 redirects\u002Fs + 100 creates\u002Fs + 10 reports\u002Fs",[390,429,430],{"align":376},"30 s",[390,432,433],{"align":376},"52.6 ms",[390,435,436],{},"Fell behind",[368,438,439,442,444,447],{},[390,440,441],{},"5,000 redirects\u002Fs to one hot link",[390,443,430],{"align":376},[390,445,446],{"align":376},"14.2 ms",[390,448,401],{},[23,450,451,452,455,456,59,459,462],{},"The 3,200 r\u002Fs test completed without HTTP errors, incorrect responses, or missed iterations. After the event workers drained, PostgreSQL contained all ",[46,453,454],{},"192,000"," redirect events: ",[46,457,458],{},"172,800 human",[46,460,461],{},"19,200 bot"," events.",[23,464,465,466,469,470,473,474,476],{},"At the 5,000 r\u002Fs mixed-workload target, k6 could not start ",[46,467,468],{},"1,023 scheduled iterations",". The test delivered about ",[46,471,472],{},"4,963 redirects\u002Fs",", and redirect p99 rose to ",[46,475,433],{},". The application didn't suddenly start returning bad redirects; the workload stopped meeting the offered rate and latency target.",[23,478,479,480,484,485,488,489,492],{},"The difference between ",[481,482,483],"em",{},"offered",", ",[481,486,487],{},"completed",", and ",[481,490,491],{},"correctly persisted"," requests matters. I tried to measure all three.",[23,494,495,496,499],{},"Link creation exposed a different limit. When offered 3,472 creations\u002Fs, the test delivered around ",[46,497,498],{},"1,388\u002Fs",", with PostgreSQL using its allocated CPU budget. Scaling the API alone wouldn't fix that bottleneck.",[23,501,502,503,507],{},"The ",[38,504,506],{"href":505},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Fload-results.json","load results"," include every stage, the failed attempts among them.",[509,510,512],"h3",{"id":511},"one-hot-link-no-limits","One hot link, no limits",[23,514,515],{},"The runs above capped every container. To see what a single cached redirect and its click cost, I later removed the caps on the same laptop: the API ran as a native Go process, PostgreSQL 17 and Redis 7.2 ran in Docker without limits, and k6 sent every request to one link over plain HTTP. Each step ramped for 20 seconds, then held its target for 60.",[362,517,518,540],{},[365,519,520],{},[368,521,522,525,528,531,534,537],{},[371,523,524],{},"Target",[371,526,527],{"align":376},"Delivered during hold",[371,529,530],{"align":376},"Errors",[371,532,533],{"align":376},"p95",[371,535,536],{"align":376},"p99",[371,538,539],{"align":376},"Clicks stored",[385,541,542,561],{},[368,543,544,547,549,552,555,558],{},[390,545,546],{},"4,000\u002Fs",[390,548,546],{"align":376},[390,550,551],{"align":376},"0",[390,553,554],{"align":376},"3.2 ms",[390,556,557],{"align":376},"8.8 ms",[390,559,560],{"align":376},"283,945 of 283,945",[368,562,563,568,573,577,582,587],{},[390,564,565],{},[46,566,567],{},"10,000\u002Fs",[390,569,570],{"align":376},[46,571,572],{},"~9,800\u002Fs",[390,574,575],{"align":376},[46,576,551],{},[390,578,579],{"align":376},[46,580,581],{},"26.8 ms",[390,583,584],{"align":376},[46,585,586],{},"61.3 ms",[390,588,589],{"align":376},[46,590,591],{},"698,171 of 698,171",[23,593,594],{},"Each redirect still recorded a click, so at the 10,000\u002Fs target the Redis stream and PostgreSQL absorbed about 9,800 click events a second. After each run, PostgreSQL held one click for every request k6 sent, warm-up included, and the stream had drained without a single failed event. An earlier attempt at the same target reached about 9,560\u002Fs with p95 39.3 ms; every click was stored there too.",[23,596,597],{},[598,599],"img",{"alt":600,"src":601},"k6 summary for the 10,000 redirects\u002Fsecond target: 588,390 requests in the held minute, 0% failed, p95 26.8 ms","\u002Farticles\u002Fbuilding-shortened-cv\u002Fk6-local-10000.png",[23,603,604,605,608],{},"The 10,000\u002Fs target wasn't fully met: k6 couldn't schedule ",[46,606,607],{},"11,829 iterations",", almost all of them during the held minute. While the 4,000\u002Fs step ran, I sampled each process's CPU once a second:",[362,610,611,624],{},[365,612,613],{},[368,614,615,618,621],{},[371,616,617],{},"Process",[371,619,620],{"align":376},"Cores at 4,000\u002Fs",[371,622,623],{"align":376},"Per 1,000\u002Fs",[385,625,626,637,648,659],{},[368,627,628,631,634],{},[390,629,630],{},"Go API, including the click workers",[390,632,633],{"align":376},"0.70",[390,635,636],{"align":376},"0.18",[368,638,639,642,645],{},[390,640,641],{},"PostgreSQL",[390,643,644],{"align":376},"0.34",[390,646,647],{"align":376},"0.09",[368,649,650,653,656],{},[390,651,652],{},"Redis",[390,654,655],{"align":376},"0.02",[390,657,658],{"align":376},"under 0.01",[368,660,661,664,667],{},[390,662,663],{},"k6",[390,665,666],{"align":376},"1.53",[390,668,669],{"align":376},"0.38",[23,671,672,673,676],{},"Scaled in a straight line, 10,000\u002Fs would need about ",[46,674,675],{},"2.7 cores"," for the API, PostgreSQL, and Redis, plus 3.8 for k6. That's more than a four-core laptop can spare while it also runs my editor and browser, which fits what happened: k6 fell behind, and the API never returned an error. It also suggests a dedicated four-core machine could serve 10,000 cached redirects a second with the load generator elsewhere. That's an estimate, not a measurement.",[23,678,679,680,59,683,686,687,59,691,695,696,700],{},"These runs show the cost of a cached redirect and its click, and nothing more. They don't cover cold links, new TLS connections, or production traffic, and they aren't comparable to the capped runs above. The ",[38,681,546],{"href":682},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Flocal-redirect\u002Fsummary-4000.json",[38,684,567],{"href":685},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Flocal-redirect\u002Fsummary-10000.json"," summaries, the ",[38,688,690],{"href":689},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Flocal-redirect\u002Fcpu-4000-pidstat.txt","per-process",[38,692,694],{"href":693},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Flocal-redirect\u002Fcpu-4000-mpstat.txt","host"," CPU samples, and the ",[38,697,699],{"href":698},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Flocal-redirect\u002Fenvironment.json","environment"," are published with the rest of the evidence.",[83,702,704],{"id":703},"the-failure-tests-taught-me-more-than-the-throughput-tests","The failure tests taught me more than the throughput tests",[23,706,707],{},"I deliberately interrupted parts of the stack to find out what survived:",[106,709,710,720,726,732],{},[109,711,712,715,716,719],{},[46,713,714],{},"PostgreSQL unavailable:"," Warm links kept redirecting at 500 r\u002Fs during a roughly six-second outage. Uncached links returned ",[27,717,718],{},"503"," until the database came back; the completed test's click events were reconciled afterward.",[109,721,722,725],{},[46,723,724],{},"Redis unavailable:"," The two-instance rehearsal saw redirect tail latency rise from around 2 ms to 51 ms. I added backoff so requests wouldn't each wait on an already failing Redis call.",[109,727,728,731],{},[46,729,730],{},"API crash:"," One recovery test reclaimed all 100 sampled queued clicks after about 61 seconds. But a separate two-instance failure rehearsal lost four clicks from the killed API's memory buffer and recorded 58 incorrect responses across its broader fault stage.",[109,733,734,737],{},[46,735,736],{},"Cache invalidation:"," A failed revocation exposed the risk of serving a deleted link from stale cache state. Local revocation tracking and retry tests came out of that investigation.",[23,739,740,741,745,746,750],{},"These weren't all the same experiment, and I don't interpret one successful recovery sample as a guarantee. They gave me concrete bugs to fix and failure modes to document. The ",[38,742,744],{"href":743},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Ffault-checks.json","single-instance fault checks"," and the ",[38,747,749],{"href":748},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Ftwo-instance-rehearsal.md","two-instance rehearsal"," record each set of conditions and outcomes.",[83,752,754],{"id":753},"then-i-tested-the-live-vps","Then I tested the live VPS",[23,756,757,758,761,762,765],{},"The live service runs through ",[46,759,760],{},"Traefik and HTTPS"," on a ",[46,763,764],{},"four-vCPU Contabo VPS",", managed with Coolify and shared with two other applications.",[23,767,768,769,772],{},"This test was intentionally different from the laptop benchmark: k6 exercised one warm short link through the real public endpoint, ramping to each rate for 20 seconds before holding it for a minute. But k6 still ran ",[46,770,771],{},"on the same VPS",", competing with Traefik, the API, Redis, and PostgreSQL for CPU.",[362,774,775,794],{},[365,776,777],{},[368,778,779,782,785,788,791],{},[371,780,781],{},"Offered redirect rate",[371,783,784],{"align":376},"Approx. delivered during hold",[371,786,787],{"align":376},"Reported p95",[371,789,790],{"align":376},"Reported p99",[371,792,793],{"align":376},"Completed HTTP errors",[385,795,796,812,828],{},[368,797,798,801,804,807,810],{},[390,799,800],{},"1,000\u002Fs",[390,802,803],{"align":376},"~1,000\u002Fs",[390,805,806],{"align":376},"13.8 ms",[390,808,809],{"align":376},"39.6 ms",[390,811,551],{"align":376},[368,813,814,817,820,823,826],{},[390,815,816],{},"2,000\u002Fs",[390,818,819],{"align":376},"~1,978\u002Fs",[390,821,822],{"align":376},"68.1 ms",[390,824,825],{"align":376},"152 ms",[390,827,551],{"align":376},[368,829,830,833,836,839,841],{},[390,831,832],{},"3,000\u002Fs",[390,834,835],{"align":376},"Not sustained; run aborted during ramp",[390,837,838],{"align":376},"—",[390,840,838],{"align":376},[390,842,838],{"align":376},[23,844,845,846,849],{},"The 2,000 r\u002Fs attempt had ",[46,847,848],{},"462 unsent iterations across the full ramp-and-hold run",", even though the completed requests returned the expected redirect. At 3,000 r\u002Fs, the shared host became CPU-saturated and latencies grew to seconds before the test stopped.",[23,851,852,853,856,857,860],{},"At the 2,000 target, the Go API used roughly ",[46,854,855],{},"0.5 vCPU and 115 MB of memory",", while Traefik and k6 took about a vCPU each. That's encouraging for the redirect handler, but it ",[46,858,859],{},"doesn't establish the API's standalone limit or a production capacity guarantee",". These requests reused TLS connections and all targeted one warm link.",[23,862,863],{},"The held-window figures are also approximate. The script used for this run started its warm-up clock separately for each k6 virtual user, so users added mid-run had their first 20 seconds left out of the measured minute. The script now times the warm-up from the scenario start.",[23,865,866,867,59,871,875,876,30],{},"So I can say the deployed system served a substantial amount of traffic through HTTPS with correct responses. I can't yet say what it will sustain under realistic visitors and a separate load generator. The ",[38,868,870],{"href":869},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Fproduction-environment.json","production environment",[38,872,874],{"href":873},"\u002Farticles\u002Fbuilding-shortened-cv\u002Fevidence\u002Fproduction\u002Fsummary-2000.json","held-minute results"," are published alongside the CPU samples and scripts in the ",[38,877,880],{"href":878,"rel":879},"https:\u002F\u002Fgithub.com\u002FIbrahimIjai\u002Fsh.sh\u002Ftree\u002Fmain\u002Fbenchmarks",[42],"benchmarks folder",[83,882,884],{"id":883},"what-im-taking-away","What I'm taking away",[23,886,887],{},"This project started as a URL shortener and turned into a practical lesson in how distributed systems behave under pressure.",[23,889,890],{},"The local cache made hot redirects inexpensive. Redis Streams separated click ingestion from the HTTP path and, locally, kept up with about 9,800 clicks a second without losing one. Event IDs made retries safe from duplicate counting. Daily rollups kept reporting work bounded. And failure testing uncovered bugs that a healthy-system benchmark never would have found.",[23,892,893],{},"Next, I want to re-run the live test with the corrected script from a separate machine, test a larger mix of cold and hot links, and rehearse restoring PostgreSQL from backup.",[23,895,896],{},"The source code, benchmark scripts, successful runs, and failed attempts are public:",[106,898,899,908,918],{},[109,900,901,904,905],{},[46,902,903],{},"Project:"," ",[38,906,43],{"href":40,"rel":907},[42],[109,909,910,904,913],{},[46,911,912],{},"Source code:",[38,914,917],{"href":915,"rel":916},"https:\u002F\u002Fgithub.com\u002FIbrahimIjai\u002Fsh.sh",[42],"IbrahimIjai\u002Fsh.sh",[109,919,920,904,923],{},[46,921,922],{},"Benchmark evidence:",[38,924,926],{"href":878,"rel":925},[42],"benchmarks\u002F",[23,928,929],{},"If you're working on similar caching, analytics, or reliability problems, I'd be interested in your feedback.",[931,932,933],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":135,"searchDepth":935,"depth":935,"links":936},2,[937,938,939,940,941,942,943,947,948,949],{"id":85,"depth":935,"text":86},{"id":123,"depth":935,"text":124},{"id":154,"depth":935,"text":155},{"id":190,"depth":935,"text":191},{"id":235,"depth":935,"text":236},{"id":291,"depth":935,"text":292},{"id":332,"depth":935,"text":333,"children":944},[945],{"id":511,"depth":946,"text":512},3,{"id":703,"depth":935,"text":704},{"id":753,"depth":935,"text":754},{"id":883,"depth":935,"text":884},"Backend Engineering",null,"2026-10-12","Building shortened.cv: caching, asynchronous analytics, failure recovery, and the limits I found when I tested it locally and on a live VPS.","md",false,{},true,"\u002Farticles\u002Fbuilding-shortened-cv",{"title":5,"description":953},"articles\u002Fbuilding-shortened-cv",[962,963,641,652,964,965],"Go","Gin","System Design","Performance","1VVRFC_IHkHvzM2VYJzHa5YuYGhQvNtfDOD0JbwcPIA",1791997974079]