<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Lucjan Suski on Medium]]></title>
        <description><![CDATA[Stories by Lucjan Suski on Medium]]></description>
        <link>https://medium.com/@lucjan?source=rss-e5567a345697------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*r2f_tj2i-D4SmJhCfbF44w.jpeg</url>
            <title>Stories by Lucjan Suski on Medium</title>
            <link>https://medium.com/@lucjan?source=rss-e5567a345697------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 05:34:45 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@lucjan/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Load data from Stripe to BigQuery]]></title>
            <link>https://lucjan.medium.com/load-data-from-stripe-to-bigquery-aa9a4533ddd1?source=rss-e5567a345697------2</link>
            <guid isPermaLink="false">https://medium.com/p/aa9a4533ddd1</guid>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[bigquery]]></category>
            <category><![CDATA[stripe]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <category><![CDATA[etl]]></category>
            <dc:creator><![CDATA[Lucjan Suski]]></dc:creator>
            <pubDate>Sat, 31 Jul 2021 18:44:12 GMT</pubDate>
            <atom:updated>2021-08-03T18:45:07.808Z</atom:updated>
            <content:encoded><![CDATA[<p>Here’s the easiest way to synchronize the data from Stripe to BigQuery, under the condition you have <strong>enabled</strong> <a href="https://stripe.com/sigma"><strong>Stripe Sigma</strong></a><strong> (paid but worth it!)</strong>.</p><h3>1. Create Cloud Function that handles Sigma webhooks</h3><p>Start by creating new Cloud Function.</p><h4>Runtime variables</h4><p>Make sure to add STRIPE_SECRET runtime variable:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v2UG26mIUW63eMrsR6gQjg.png" /></figure><h4>index.js</h4><pre>const { Storage } = require(&quot;<a href="http://twitter.com/google">@google</a>-cloud/storage&quot;);<br>const { BigQuery } = require(&quot;<a href="http://twitter.com/google">@google</a>-cloud/bigquery&quot;);<br>const fetch = require(&quot;node-fetch&quot;);</pre><pre>const storage = new Storage();<br>const bigquery = new BigQuery();<br>const bucket = &quot;your-cloud-storage-bucket&quot;;</pre><pre>async function uploadFile(fetchPromise, bucket, filename) {<br>  let result = await fetchPromise;<br>  return new Promise((resolve, reject) =&gt; {<br>    const readStream = result.body;<br>    const writeStream = storage<br>      .bucket(bucket)<br>      .file(filename)<br>      .createWriteStream();</pre><pre>readStream.on(&quot;error&quot;, reject);<br>    writeStream.on(&quot;error&quot;, reject);<br>    writeStream.on(&quot;finish&quot;, () =&gt; resolve(true));</pre><pre>readStream.pipe(writeStream);<br>  });<br>}</pre><pre>exports.upload = async (req, res) =&gt; {<br>  if (req.body.type === &quot;sigma.scheduled_query_run.created&quot;) {<br>    const [bq, _project, dataset, table] =<br>      req.body.data.object.title.split(&quot;:&quot;);<br>    if (bq !== &quot;bigquery&quot;) {<br>      return res.status(200).send(`Ignoring ${req.body.data.object.title}. No bigquery: prefix`);<br>    }</pre><pre>const url = req.body.data.object.file.url;<br>    const fetchPromise = fetch(url, {<br>      headers: {<br>        Authorization:<br>          &quot;Basic &quot; + Buffer.from(process.env.STRIPE_SECRET).toString(&quot;base64&quot;),<br>      },<br>    });<br>    const filename =<br>      req.body.data.object.id + &quot;/&quot; + req.body.data.object.file.id;<br>    await uploadFile(fetchPromise, bucket, filename);<br>    const metadata = {<br>      sourceFormat: &quot;CSV&quot;,<br>      skipLeadingRows: 1,<br>      autodetect: true,<br>      location: &quot;EU&quot;,<br>    };<br>    try {      <br>     const [exists] = await bigquery.dataset(dataset).table(table).exists();<br>     if (exists) {<br>        await bigquery.dataset(dataset).table(table).delete();<br>      }<br>      await bigquery<br>        .dataset(dataset)<br>        .table(table)<br>        .load(storage.bucket(bucket).file(filename), metadata);<br>    } catch (e) {<br>      return res.status(500).send(JSON.stringify(e));<br>    }</pre><pre>return res.status(200).send(req.body.data.object.file.id);<br>  }<br>  return res.status(500).send(&quot;Failed&quot;);<br>};</pre><h4>package.json</h4><pre>{<br>  &quot;name&quot;: &quot;stripe-sigma&quot;,<br>  &quot;version&quot;: &quot;0.0.1&quot;,<br>  &quot;dependencies&quot;: {<br>    &quot;<a href="http://twitter.com/google">@google</a>-cloud/storage&quot;: &quot;^5.11.0&quot;,<br>    &quot;<a href="http://twitter.com/google">@google</a>-cloud/bigquery&quot;: &quot;^5.7.0&quot;,<br>    &quot;node-fetch&quot;: &quot;^2.6.1&quot;<br>  }<br>}</pre><p>Deploy this function and copy the trigger URL:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ii-LeIeX9ffyZWqRkdbwYw.png" /></figure><h3>2. Add Webhook endpoint</h3><p>Go to Webhooks and create event, providing Endpoint URL from previous step and selecting sigma_scheduled_query_run.created event:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RJy3IoD48VW2Ku7X1BnqPQ.png" /></figure><h3>3. Create Sigma Scheduled query</h3><p>Go to Sigma dashboard (<a href="https://dashboard.stripe.com/sigma/queries">https://dashboard.stripe.com/sigma/queries</a>)</p><p>Create a new query with any SQL you like (here I’m just loading all the charges data) and assign a name according to the convention: bigquery:project_id:dataset_id:table_name</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*umsQD8_L1otKbQ95halKXw.png" /></figure><p>Schedule this query using “<strong>Schedule</strong>” button, you can leave out emails so that you don’t get notified in any other way than Webhook.</p><p>From now on, the results from this query will be saved under stripe_charges table in segmentation dataset.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aa9a4533ddd1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Choose Exciting Technology]]></title>
            <link>https://lucjan.medium.com/choose-exciting-technology-e735bba08acc?source=rss-e5567a345697------2</link>
            <guid isPermaLink="false">https://medium.com/p/e735bba08acc</guid>
            <category><![CDATA[startup-lessons]]></category>
            <category><![CDATA[entrepreneurship]]></category>
            <category><![CDATA[startup]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Lucjan Suski]]></dc:creator>
            <pubDate>Sun, 24 Jan 2021 21:32:36 GMT</pubDate>
            <atom:updated>2021-01-24T21:48:10.245Z</atom:updated>
            <content:encoded><![CDATA[<p>There is a popular movement on Choosing Boring Technology started by <a href="https://mcfunley.com/choose-boring-technology">article by Dan McKinley</a>, followed by <a href="https://news.ycombinator.com/item?id=20323246">some</a> <a href="https://news.ycombinator.com/item?id=23444594">discussions</a> on <a href="https://news.ycombinator.com/item?id=25322651">Hacker</a> <a href="https://news.ycombinator.com/item?id=9291215">News</a>. While I agree that it can be good rule of thumb from a business perspective, I want to shed some light on other angles and why I think it shouldn’t become universal.</p><h3>Slowing down innovation</h3><p>Choosing Boring Technology can lead to slowing down innovation. It started recently — in 2015. It may be hard to notice because it’s fresh, but having similar movement going popular 20 years earlier could be disastrous. Many valuable languages and technologies (Ruby, .NET, Rust, to name a few) would have a really hard time becoming popular. Language or technology, no matter how good it is, cannot thrive without adoption and community. <em>Especially</em> early on in its life. Choose Boring Technology manifesto stands in opposition to early adoption and may slow down innovation.</p><h3>Joy of programming</h3><p>The choice of some new framework, technology or language can make us slower in delivering value. It can make it harder to maintain the codebase in the future. Fewer developers know it. All of it matters from a short-term business perspective. But there is more to life and work than just maximizing the business value of everything we do. It’s important to keep business in mind and optimize ROI, but it’s too easy to fall into the trap of <em>optimizing too much</em>.</p><p>This new exciting thing will make our team learn a lot faster than something well-established. It will give a new perspective to what we already know. It can help to attract talents. It can bring more joy to programming. Sometimes business risk outweighs all those, but it’s rare. We tend to overestimate business risk, and I write it from an entrepreneur’s perspective.</p><h3>Case in point</h3><p>I help to build Surfer as co-founder and CTO for over three years now. It started as a side project, with a simple idea of creating better SEO tooling, now it’s a $3M ARR, entirely bootstrapped company, hiring 30 people. Our primary goal for this project was <em>to learn</em>. I was an experienced Ruby developer at that point. Learning JavaScript gave me a completely new perspective on programming and made me a better developer in Ruby and everything else. I knew Choosing Exciting Technology was a way to go.</p><p>Elixir became Surfer’s foundation. It wasn’t <em>completely </em>new territory for me — syntax is similar to Ruby, and I knew some functional programming from JavaScript, but it was Exciting, not Boring. Some would say I should have chosen Rails. Three years later, I know choosing Elixir was one of the primary reasons why we thrive. It was an excellent choice with many benefits.</p><p>I learned a lot. It kept me motivated through hard times with no customers, as I kept saying to myself if this won’t work, at least I’d learn something. Later on, it was a magnet on great talents which would be hard to acquire otherwise. Technology, especially a niche one, can be a good filter for hiring developers. If someone is interested in Elixir, we can assume they have curiosity, an essential feat for a programmer.</p><p>Did it make us slower in the beginning? Sure. But it kept us motivated when it was tough. Is it harder to hire Elixir developer than Ruby developer? Yep. But <em>all</em> of the programmers we interviewed were exceptional.</p><p>Don’t follow rules of thumb blindly. If excitement drives you, you are not alone. Join Choose Exciting Technology club.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e735bba08acc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Free URL shortener with Cloudflare Workers]]></title>
            <link>https://lucjan.medium.com/free-url-shortener-with-cloudflare-workers-125eaf87b1ec?source=rss-e5567a345697------2</link>
            <guid isPermaLink="false">https://medium.com/p/125eaf87b1ec</guid>
            <dc:creator><![CDATA[Lucjan Suski]]></dc:creator>
            <pubDate>Sun, 03 Jan 2021 23:11:19 GMT</pubDate>
            <atom:updated>2021-06-03T17:51:53.155Z</atom:updated>
            <content:encoded><![CDATA[<p>After reading that <a href="https://ylukem.com/blog/url-shorteners-set-ad-tracking-cookies">URL shorteners set ad tracking cookies</a> today, I decided to share how to get a decent shortener set up in a few minutes for free on Cloudflare Workers. It has a free tier of 100k requests per day, which is more than enough for most use cases.</p><p>To set it up, you will need a domain, but if you need an URL shortener you probably have some domain already. We used go.surferseo.com subdomain which can make a good default.</p><p>The code to get it working is just a few lines of JavaScript:</p><pre>const redirects = new Map([<br>  [&quot;cloudflare-shortener&quot;, &quot;<a href="https://lucjan.medium.com/free-url-shortener-with-cloudflare-workers-125eaf87b1ec">https://lucjan.medium.com/free-url-shortener-with-cloudflare-workers-125eaf87b1ec</a>&quot;],<br>  [&quot;neil-pattel-copied-my-design&quot;, &quot;<a href="https://lucjan.medium.com/how-neil-patel-copied-my-design-and-im-not-even-mad-ede6e99648c0">https://lucjan.medium.com/how-neil-patel-copied-my-design-and-im-not-even-mad-ede6e99648c0</a>&quot;],<br>  [&quot;surfer&quot;, &quot;<a href="https://lucjan.medium.com/how-neil-patel-copied-my-design-and-im-not-even-mad-ede6e99648c0">https://s</a>urferseo.com&quot;],<br>])</pre><pre>addEventListener(&#39;fetch&#39;, event =&gt; {<br>  event.respondWith(handleRedirect(event.request));<br>})</pre><pre>async function handleRedirect(request) {<br>  let pathname = new URL(request.url).pathname.replace(&quot;/&quot;, &quot;&quot;);<br>  let location = redirects.get(pathname);<br>  return location <br>    ? Response.redirect(location, 301) <br>    : new Response(&quot;Not Found&quot;, {status: 404})<br>}</pre><p>In this case, whenever I go to go.surferseo.com/cloudflare-shortener, I’d be redirected to this very article. Handling another path is just a matter of adding one line of JS (which could be further simplified into Cloudflare KV store to make non-technical people lives easier).</p><h3>How to get it working</h3><p>Once you add your domain to Cloudflare, go to Workers tab:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UuWa_QD5WLy7QXkw1edyiQ.png" /></figure><p>then Manage Workers:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UhgaZJmCMo_14a6htCIN6A.png" /></figure><p>Create a Worker:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/870/1*a0Vv467nuWj2X3oh8UGZZg.png" /></figure><p>Replace the JS code with the one above and <strong>hit Save and Deploy, then confirm:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/854/1*8FW7KVFFGitwNQ9AqLhapg.png" /></figure><p>Your shortener will be available under *.workers.dev domain and you can test if everything works fine.</p><p>If it does, go back to the Workers tab in your domain settings and hit Add route just under Manage Workers, typing the subdomain (or naked domain) and selecting the worker you just created.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mx5G5u358moILjP23dMlaw.png" /></figure><p>The last step is to add dummy, proxied (orange cloud) subdomain in the DNS tab, so the worker can pick it up:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0VzHDHMqhcgh4PAFBE6tIg.png" /></figure><p>If I managed to explain the steps well and you could follow through, you should have your very own, simple and free URL Shortener working!</p><p>To add, remove or change routes, just head back to Manage Workers, click the worker you created and then Quick Edit:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jyfwFPzRbshTc5mMvHSYcg.png" /></figure><p>After changing the routes and hitting Save and Deploy (don’t forget about comma after each one), it will be available in a few seconds.</p><p>Have fun!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=125eaf87b1ec" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Neil Patel copied my design and I’m not even mad]]></title>
            <link>https://lucjan.medium.com/how-neil-patel-copied-my-design-and-im-not-even-mad-ede6e99648c0?source=rss-e5567a345697------2</link>
            <guid isPermaLink="false">https://medium.com/p/ede6e99648c0</guid>
            <category><![CDATA[product]]></category>
            <category><![CDATA[saas]]></category>
            <category><![CDATA[ux]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[marketing]]></category>
            <dc:creator><![CDATA[Lucjan Suski]]></dc:creator>
            <pubDate>Wed, 27 May 2020 18:16:12 GMT</pubDate>
            <atom:updated>2020-05-27T18:20:04.787Z</atom:updated>
            <content:encoded><![CDATA[<p>Back in 2019, the Keyword Surfer Chrome extension was brought to life.</p><p>Our free SEO tool got excellent traction fast — just after seven months, we have 100k+ people using it for their SEO.</p><p>People raved about Keyword Surfer from the day one, and in April 2020, we introduced the second version of the tool.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZpVhTaYoKbspwoEX.png" /><figcaption>Keyword Surfer 2.0</figcaption></figure><p>Is this all a great validation of our work? Yes, definitely.</p><p>The fun part is: I was the only one who coded and <em>designed</em> the first version of Keyword Surfer that attracted the first 60k users. Me. Developer at my very core. It looked okay-ish, but you could tell it wasn’t touched by a real designer.</p><p>You cannot imagine how shocked, surprised, and kind of amused I was when I noticed that someone took great inspiration from my work and created an extension that could be easily confused with Keyword Surfer… its first version to be specific.</p><p>Yesterday I stumbled upon a blogpost <a href="https://neilpatel.com/blog/ubersuggest-chrome-extension/">announcing</a> Ubersuggest Chrome Extension, and I could hardly believe my own eyes.</p><p>I was staring at the screenshot showing the tool, which was literally how <a href="https://surferseo.com/keyword-surfer-extension/">Keyword Surfer</a> looked like in its previous version! See it for yourself:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oSdNCJFoiwjEhBQO.png" /><figcaption>Neil Pattel’s Ubersuggest</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VI-p6o9HQ3dKJ7PN.png" /><figcaption>Surfer’s Keyword Surfer</figcaption></figure><p>Apart from some visual details (colors, typography), it’s an extremely close look-alike!</p><p>Now, don’t get me wrong. I don’t claim that placing data directly in Google results was my idea. There are other extensions out there that enrich Google’s results with data. Many of them were created before Keyword Surfer.</p><p>BUT THIS!</p><p>A grey bar under each page title in search results with data. <em>Backlink and visibility correlation</em> charts on the right. Turn on and off switch. Settings placement. It looks like a freaking clone!</p><p>I can see how, in many ways, Neil Patel’s extension is better than Keyword Surfer 1.0, which proves they didn’t just brainlessly copy our work. (Fortunately, Keyword Surfer 2.0 is better in many ways than Neil Patel’s tool as well 😉)</p><p>And while I feel surprised… I am not mad at all.</p><p>As I mentioned, I don’t have a lot of UI/UX design experience — I’m a developer who just wears a lot of hats as a startup co-founder. If such a <a href="https://www.kissmetricshq.com/">successful</a> <a href="https://neilpatel.com/ubersuggest/">entrepreneur</a> releases a tool that closely mimics my design, it means it’s not that bad at all!</p><p>In fact, it’s not the first time someone “took inspiration” from something I’ve designed. And I’m okay with that. If anything, it proves that it’s a good design, and I feel really flattered when it happens.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DfsEvHjRHYcD8Gok.png" /><figcaption>Website Auditor’s Content editor to the left, Surfer’s Content Editor to the right. It seems we figured out a good name too!</figcaption></figure><h3>“Is it okay?”</h3><p>If you asked me if it was okay to copy the look and feel of other tools like this, this would be my answer:</p><p>Personally, I wouldn’t do that. Not because it’s fair or not. Those who decided to copy our design, made a business decision basing on fragmented information.</p><p>We implemented those designs months ago, when we were a really small team, with no designer on board. We knew we would iterate the interface as soon as it is possible. We collected little to no feedback before the release. As we have been flooded with feedback, Keyword Surfer 2.0 was born — based on data and real needs of real people.</p><p>When you copy someone’s design (or work in general), you don’t know the process that led to this design. And truth to be told, our process was focused on fast delivery while having no <em>real </em>designer on board.</p><p>So I’m not mad that Neil Patel copied my design. I am just endlessly happy Keyword Surfer 2.0 looks as cool as it does.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ede6e99648c0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Investigating WebSocket/HAProxy reconnecting and timeouts]]></title>
            <link>https://lucjan.medium.com/investigating-websocket-haproxy-reconnecting-and-timeouts-6d19cc0002a1?source=rss-e5567a345697------2</link>
            <guid isPermaLink="false">https://medium.com/p/6d19cc0002a1</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[websocket]]></category>
            <category><![CDATA[haproxy]]></category>
            <dc:creator><![CDATA[Lucjan Suski]]></dc:creator>
            <pubDate>Thu, 02 May 2019 12:35:38 GMT</pubDate>
            <atom:updated>2020-06-10T09:08:43.453Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to fix WebSockets/HAProxy timeout</h3><p>If you encounter the problem of WebSockets connection handled with HAProxy being lost, <strong>increasing the tunnel timeout to 60s</strong> or disabling it using 0 will probably fix the problem. If you’d like to know why does it happen, read along!</p><h3>Context</h3><p>First of all, let’s start with how our single-page app communicates with the backend, which is necessary to understand what happened.</p><p>Instead of a standard, XHR approach, we use Phoenix channels as a sort of RPC for our backend. When user logs in to Surfer, we establish a WebSocket connection using Phoenix JS library:</p><pre>this.socket = new Socket(host(), {<br>  params: { token: this.props.currentUser.token },<br>  reconnectAfterMs: () =&gt; 1000,<br>})<br>this.socket.connect()</pre><p>After that we establish a single channel for given user to handle all the communication:</p><pre>this.channel =<br>  this.socket.channel(`room:${this.props.currentUser.id}`)<br>this.channel.join()</pre><p>When we need to make a request to the backend , we <em>channel.push</em> the data and <em>receive</em> the response:</p><pre>this.channel.push(&#39;billing:status&#39;, {}).receive(&#39;ok&#39;, billing =&gt; {<br>  this.setState({ billing })<br>})</pre><h3>Problem</h3><p>One day we noticed some of the requests seem to fail, ending up with endless loading spinner. After refreshing the page it was clear that <strong>responses were successful</strong>, so the problem was with receiving the response and not with the request itself. We also noticed that our <strong>WebSocket is reconnecting every few seconds</strong>. Both problems were visible only on staging environment with production and local being untouched.</p><p>After some time scratching our heads and trying different things, we recalled we’ve made some changes in HAProxy configuration recently to support the downtime for a migration we wanted to make in the next few days. The change was simple — <strong>we reduced server and client timeout</strong> to make sure user won’t wait too long for our custom 503 page to appear:</p><pre>-  timeout client 50000ms<br>-  timeout server 50000ms<br>-  retries 50<br>+  timeout client 3000ms<br>+  timeout server 3000ms<br>+  retries 1</pre><p>After rolling it back it started to work perfectly again, but we still weren’t 100% sure why. Using the shorter timeout, the channel was reconnecting every 3 seconds and some requests failed to receive the response, but <strong>why it wasn’t the case for the 50 seconds timeout?</strong></p><h3>Cause</h3><p>In order to keep the WebSocket connection alive, <strong>Phoenix Socket library sends a heartbeat message which happens every 30 seconds by default</strong>. With 50 seconds timeout for the client/server everything worked fine, reducing the timeout to merely 3 seconds made WebSocket connection to timeout, so that when request took any longer, the frontend was unable to receive the response on time.</p><h3>Solution</h3><p>Obvious one was to reduce the heartbeat interval to something less than 3 seconds to make sure we keep the connection alive even with the migration config, but it wasn’t really appealing to spam the server such often.</p><p>After some googling we figured out there was another HAProxy timeout setting which is responsible for a tunnel connections:</p><blockquote>The tunnel timeout applies when a bidirectional connection is established<br>between a client and a server, and the connection remains inactive in both<br>directions<br><a href="https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#4-timeout%20tunnel">https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#4-timeout%20tunnel</a></blockquote><p><strong>When connection becomes a tunnel (as it happens for WebSockets) this timeout setting supersedes both the client and server timeouts.</strong></p><p>Here’s how it looks in the config:</p><pre>timeout client 3000ms<br>timeout server 3000ms<br>timeout tunnel 50000ms</pre><p>The proper solution was to keep the client/server timeouts low for the period of migration but set the tunnel timeout separately, so that when the app goes back up it will work fine immediately, giving us time to deploy the old HAProxy settings (which are needed to properly handle regular deployments with shorter downtimes, not giving 503 when they happen).</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6d19cc0002a1" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>