<?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>Forem: john jewski</title>
    <description>The latest articles on Forem by john jewski (@john_jewskiz).</description>
    <link>https://forem.com/john_jewskiz</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3770002%2F19f3fb04-c185-4932-b550-0dd5bd8c32d4.png</url>
      <title>Forem: john jewski</title>
      <link>https://forem.com/john_jewskiz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/john_jewskiz"/>
    <language>en</language>
    <item>
      <title>Why I Stopped Writing Video Files to Disk and Route Everything Through /tmp</title>
      <dc:creator>john jewski</dc:creator>
      <pubDate>Sun, 22 Feb 2026 21:35:12 +0000</pubDate>
      <link>https://forem.com/john_jewskiz/why-i-stopped-writing-video-files-to-disk-and-route-everything-through-tmp-1nm5</link>
      <guid>https://forem.com/john_jewskiz/why-i-stopped-writing-video-files-to-disk-and-route-everything-through-tmp-1nm5</guid>
      <description>&lt;p&gt;When I built the first version of dltkk.to I made the obvious choice — download the video file, save it to a downloads folder, stream it to the user, clean up on a timer.&lt;/p&gt;

&lt;p&gt;Within a week I had three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Disk filling up when cleanup timers misfired on server restarts&lt;/li&gt;
&lt;li&gt;Stale files from failed downloads accumulating&lt;/li&gt;
&lt;li&gt;Potential privacy issue if a user's file sat on disk longer than it should&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's how I fixed all three by routing everything through &lt;code&gt;/tmp&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Original Architecture (Wrong)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Old approach — save to disk, cleanup later&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./downloads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`video_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;.mp4`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ytdlp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yt-dlp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-o&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;ytdlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// What if this fails?&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Cleanup timer as fallback&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Delete files older than 10 minutes&lt;/span&gt;
  &lt;span class="c1"&gt;// But what if server restarts?&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the server restarts mid-download the cleanup never runs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;res.download&lt;/code&gt; callback doesn't guarantee the file was sent successfully&lt;/li&gt;
&lt;li&gt;Disk space is finite and permanent storage compounds&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The /tmp Architecture (Right)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;streamViaTmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tmpFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/tmp/dltkk_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.mp4`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ytdlp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yt-dlp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-o&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tmpFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nx"&gt;ytdlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Download failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attachment; filename="video.mp4"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// Delete after streaming&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Delete on error too&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Kill and cleanup if client disconnects&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writableEnded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ytdlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;cleanup&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/tmp&lt;/code&gt; is RAM-based on Linux — no actual disk writes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cleanup()&lt;/code&gt; is called in every code path — success, error, and disconnect&lt;/li&gt;
&lt;li&gt;File only exists for the duration of the download + stream — seconds at most&lt;/li&gt;
&lt;li&gt;Server restarts clear &lt;code&gt;/tmp&lt;/code&gt; automatically on most Linux systems&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stdout Trap
&lt;/h2&gt;

&lt;p&gt;Before landing on &lt;code&gt;/tmp&lt;/code&gt; I tried streaming yt-dlp output directly via stdout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yt-dlp &lt;span class="nt"&gt;-o&lt;/span&gt; - URL | pipe_to_client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks elegant but breaks silently on any format requiring merging. YouTube downloads almost always need merging — separate video stream (f137) and audio stream (f140) combined by ffmpeg into a single MP4.&lt;/p&gt;

&lt;p&gt;When you pipe to stdout, ffmpeg can't write the merged file atomically. It either fails silently or produces a corrupted file. You won't notice until a user complains their video has no audio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; anything that might need ffmpeg merging must go through a real file path, even if that file path is in RAM.&lt;/p&gt;




&lt;h2&gt;
  
  
  GIF Conversion — Two /tmp Files
&lt;/h2&gt;

&lt;p&gt;GIF conversion needs two passes — download as MP4 first, then convert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tmpMp4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/tmp/dltkk_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.mp4`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tmpGif&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/tmp/dltkk_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.gif`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Download to tmpMp4 first&lt;/span&gt;
&lt;span class="nx"&gt;ytdlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`ffmpeg -y -i "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tmpMp4&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" -vf "fps=10,scale=480:-1:flags=lanczos" -loop 0 "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tmpGif&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stream tmpGif to client&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpGif&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpMp4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Delete both&lt;/span&gt;
      &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpGif&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both files stay in RAM, both get deleted after the GIF streams. At peak load you might have a few hundred MB in /tmp — fine for a VPS with 2GB+ RAM.&lt;/p&gt;




&lt;h2&gt;
  
  
  The res.on('close') vs req.on('close') Bug
&lt;/h2&gt;

&lt;p&gt;One more trap worth mentioning. I had this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ytdlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Kills yt-dlp immediately&lt;/span&gt;
  &lt;span class="nf"&gt;cleanup&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;&lt;code&gt;req&lt;/code&gt; fires &lt;code&gt;close&lt;/code&gt; the moment the POST request body is fully read — which happens almost instantly after the request arrives. So I was spawning yt-dlp and killing it a fraction of a second later on every single download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use &lt;code&gt;res.on('close')&lt;/code&gt; — that fires only when the actual browser connection drops.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writableEnded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ytdlp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;cleanup&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Built With This
&lt;/h2&gt;

&lt;p&gt;All of the above is running in production at &lt;a href="https://dltkk.to" rel="noopener noreferrer"&gt;dltkk.to&lt;/a&gt; — a free yt-dlp web frontend supporting 1000+ platforms.&lt;/p&gt;

&lt;p&gt;Related reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dltkk.to/blog/youtube-to-mp4-converter.html" rel="noopener noreferrer"&gt;How dltkk handles YouTube to MP4 conversion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dltkk.to/blog/how-to-convert-youtube-to-mp3.html" rel="noopener noreferrer"&gt;YouTube to MP3 extraction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dltkk.to/blog/youtube-to-gif-converter.html" rel="noopener noreferrer"&gt;TikTok to GIF conversion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dltkk.to/blog/tiktok-bulk-downloader.html" rel="noopener noreferrer"&gt;Batch downloading architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions welcome in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Every TikTok Downloader Quirk I Hit Building dltkk.to (And How I Fixed Them)</title>
      <dc:creator>john jewski</dc:creator>
      <pubDate>Sun, 22 Feb 2026 21:33:03 +0000</pubDate>
      <link>https://forem.com/john_jewskiz/every-tiktok-downloader-quirk-i-hit-building-dltkkto-and-how-i-fixed-them-909</link>
      <guid>https://forem.com/john_jewskiz/every-tiktok-downloader-quirk-i-hit-building-dltkkto-and-how-i-fixed-them-909</guid>
      <description>&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%2F2g8i1mhk9l7sd8rfdp8y.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%2F2g8i1mhk9l7sd8rfdp8y.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building a TikTok downloader sounds simple until you actually try it. TikTok actively fights scrapers and downloaders at every level. Here's every problem I hit and the exact fix for each one.&lt;/p&gt;

&lt;p&gt;Problem 1: TikTok Blocks All Non-Browser Requests&lt;br&gt;
The most basic issue. Send a plain yt-dlp request to TikTok and you get a 403 immediately. TikTok checks the request signature against known browser fingerprints.&lt;br&gt;
Fix:&lt;br&gt;
bashyt-dlp --impersonate chrome-131 &lt;a href="https://www.tiktok.com/@user/video/123" rel="noopener noreferrer"&gt;https://www.tiktok.com/@user/video/123&lt;/a&gt;&lt;br&gt;
The --impersonate flag makes yt-dlp spoof a full Chrome 131 browser signature including headers, TLS fingerprint, and HTTP/2 settings. Without this nothing works.&lt;br&gt;
This needs updating periodically as TikTok updates its detection. chrome-131 is current as of early 2026.&lt;/p&gt;

&lt;p&gt;Problem 2: Format Selection Returns Wrong File&lt;br&gt;
TikTok videos have multiple quality streams. Using --format best sometimes returns a format that requires merging, which then fails silently if you're not set up for it.&lt;br&gt;
Fix:&lt;br&gt;
bashyt-dlp --impersonate chrome-131 --format b -o output.mp4 URL&lt;br&gt;
Format b (best single file, no merging) is the most reliable for TikTok. It returns a pre-merged file directly instead of separate video and audio streams.&lt;/p&gt;

&lt;p&gt;Problem 3: Audio Extraction Fails&lt;br&gt;
Extracting MP3 from TikTok using standard -x --audio-format mp3 flags sometimes produces a corrupted file.&lt;br&gt;
Fix:&lt;br&gt;
bashyt-dlp --impersonate chrome-131 --no-playlist -x --audio-format mp3 --audio-quality 0 -o output.mp3 URL&lt;br&gt;
The --audio-quality 0 flag forces the highest available bitrate. Without it you sometimes get a low quality fallback.&lt;/p&gt;

&lt;p&gt;Problem 4: Watermark on Downloaded Video&lt;br&gt;
If you use certain format selectors TikTok serves the watermarked version of the video. The watermark-free version is available but requires the right format selection.&lt;br&gt;
Fix:&lt;br&gt;
Format b (best single stream) returns the watermark-free version. The watermarked version appears when you accidentally select a different format stream.&lt;/p&gt;

&lt;p&gt;Problem 5: Private Videos Give Unhelpful Errors&lt;br&gt;
When a video is private yt-dlp returns a generic error that's hard to parse into a user-friendly message.&lt;br&gt;
Fix:&lt;br&gt;
javascriptfunction parseYtdlpError(errorOutput) {&lt;br&gt;
  if (errorOutput.includes('Private video')) return 'This video is private and cannot be downloaded.';&lt;br&gt;
  if (errorOutput.includes('not available')) return 'This video is not available in your region or has been deleted.';&lt;br&gt;
  if (errorOutput.includes('Login required')) return 'This content requires login and cannot be downloaded.';&lt;br&gt;
  return 'Download failed. Check the URL and try again.';&lt;br&gt;
}&lt;br&gt;
Parse the stderr output and map specific error strings to user-friendly messages.&lt;/p&gt;

&lt;p&gt;Problem 6: Rate Limiting Under Load&lt;br&gt;
TikTok rate limits download requests from the same IP. Under moderate traffic this starts causing failures.&lt;br&gt;
Fix:&lt;br&gt;
Rate limit on your own server before TikTok sees too many requests:&lt;br&gt;
javascriptconst rateLimitMap = new Map();&lt;br&gt;
function rateLimit(ip) {&lt;br&gt;
  const now = Date.now();&lt;br&gt;
  const timestamps = (rateLimitMap.get(ip) || []).filter(t =&amp;gt; now - t &amp;lt; 60000);&lt;br&gt;
  timestamps.push(now);&lt;br&gt;
  rateLimitMap.set(ip, timestamps);&lt;br&gt;
  return timestamps.length &amp;gt; 3; // 3 requests per minute per IP&lt;br&gt;
}&lt;br&gt;
3 requests per minute per IP keeps you under TikTok's detection threshold while still being usable.&lt;/p&gt;

&lt;p&gt;What I Built With This&lt;br&gt;
dltkk.to — a free yt-dlp web frontend that handles TikTok, YouTube, Instagram, Twitter and 1000+ other platforms. Everything above is in production.&lt;br&gt;
Relevant guides if you're building something similar:&lt;/p&gt;

&lt;p&gt;Why TikTok downloaders stop working and how to fix them&lt;br&gt;
&lt;a href="https://dltkk.to/blog/tiktok-downloader-not-working-fix.html" rel="noopener noreferrer"&gt;Why TikTok downloaders stop working and how to fix them&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dltkk.to/blog/tiktok-mp3-downloader.html" rel="noopener noreferrer"&gt;TikTok to MP3 extraction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dltkk.to/blog/tiktok-bulk-downloader.html" rel="noopener noreferrer"&gt;Batch TikTok downloading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer questions about any of this in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built a Free yt-dlp Web Frontend That Supports 1000+ Sites — Here's How</title>
      <dc:creator>john jewski</dc:creator>
      <pubDate>Sat, 21 Feb 2026 23:38:26 +0000</pubDate>
      <link>https://forem.com/john_jewskiz/i-built-a-free-yt-dlp-web-frontend-that-supports-1000-sites-heres-how-1f45</link>
      <guid>https://forem.com/john_jewskiz/i-built-a-free-yt-dlp-web-frontend-that-supports-1000-sites-heres-how-1f45</guid>
      <description>&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%2Fdxgn9o24q002uwu4153w.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%2Fdxgn9o24q002uwu4153w.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few weeks ago I got tired of every video downloader site being a minefield of fake download buttons, popup ads, and malware redirects. So I built dltkk.to — a clean, no-BS web frontend for yt-dlp that anyone can use without installing anything.&lt;br&gt;
Here's what I built, how it works technically, and what I learned shipping it.&lt;/p&gt;

&lt;p&gt;What It Does&lt;br&gt;
dltkk.to is a browser-based video downloader powered by yt-dlp on the backend. Paste any video URL, pick a format, hit download. That's it.&lt;br&gt;
Supported formats: MP4, MP3, FLAC, WAV, AAC, MKV, GIF, and thumbnail extraction.&lt;br&gt;
Supported platforms: Anything yt-dlp supports — TikTok, YouTube, Twitter/X, Instagram, Twitch, Vimeo, Facebook, and 1000+ more.&lt;br&gt;
Extra features:&lt;/p&gt;

&lt;p&gt;Batch mode — paste up to 10 URLs at once, processes sequentially&lt;br&gt;
GIF conversion — converts video to optimized GIF via ffmpeg&lt;br&gt;
No ads, no watermark, no signup — ever&lt;/p&gt;

&lt;p&gt;The Stack&lt;br&gt;
Simple Node.js + Express backend, plain HTML/CSS/JS frontend. No frameworks, no build step. Runs on a single VPS.&lt;br&gt;
javascript// Core download flow — everything routes through /tmp&lt;br&gt;
function streamViaTmp(videoUrl, platform, format, quality, req, res) {&lt;br&gt;
  const tmpFile = &lt;code&gt;/tmp/dltkk_${Date.now()}_${rand}.${outputExt}&lt;/code&gt;;&lt;/p&gt;

&lt;p&gt;const ytdlp = spawn('yt-dlp', buildArgs(platform, format, quality, tmpFile, videoUrl));&lt;/p&gt;

&lt;p&gt;ytdlp.on('close', code =&amp;gt; {&lt;br&gt;
    if (code !== 0) return res.status(500).json({ error: parseError(errorOutput) });&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Stream file to client then delete immediately
res.setHeader('Content-Disposition', `attachment; filename="dltkk.${outputExt}"`);
const stream = fs.createReadStream(tmpFile);
stream.pipe(res);
stream.on('finish', () =&amp;gt; fs.unlinkSync(tmpFile)); // deleted instantly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Key Technical Decisions&lt;br&gt;
Why /tmp Instead of a Downloads Folder&lt;br&gt;
My first version saved files to a downloads directory and cleaned them up on a timer. This caused two problems — disk space filling up under load, and files occasionally not getting cleaned up if the server restarted.&lt;br&gt;
The fix was writing everything to /tmp which is RAM-based on Linux. Files never touch the main disk, and cleanup is instant after the stream finishes. Zero disk space issues since.&lt;br&gt;
Why Not Stream Directly via stdout&lt;br&gt;
yt-dlp can pipe directly to stdout with -o - which sounds ideal for streaming. The problem is any format that requires merging separate video and audio streams (most YouTube downloads, many others) needs ffmpeg to write a temp file first. Trying to stream this causes silent failures on certain platforms.&lt;br&gt;
The solution: everything goes through /tmp. It's RAM anyway so there's no meaningful performance difference, and it works reliably across all platforms.&lt;br&gt;
GIF Conversion&lt;br&gt;
javascriptexec(&lt;code&gt;ffmpeg -i "${tmpMp4}" -vf "fps=10,scale=480:-1:flags=lanczos" -loop 0 "${tmpGif}"&lt;/code&gt;)&lt;br&gt;
Download as MP4 first, convert with ffmpeg, stream the GIF, delete both files. The lanczos scaling algorithm gives the best quality at small file sizes. fps=10 keeps the GIF smooth without bloating the file.&lt;br&gt;
Rate Limiting&lt;br&gt;
3 requests per minute per IP using a simple in-memory Map. Cleans itself up every 5 minutes. Good enough for the current traffic level.&lt;br&gt;
javascriptconst rateLimitMap = new Map();&lt;br&gt;
function rateLimit(ip) {&lt;br&gt;
  const now = Date.now();&lt;br&gt;
  const timestamps = (rateLimitMap.get(ip) || []).filter(t =&amp;gt; now - t &amp;lt; 60000);&lt;br&gt;
  timestamps.push(now);&lt;br&gt;
  rateLimitMap.set(ip, timestamps);&lt;br&gt;
  return timestamps.length &amp;gt; 3;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Platform-Specific Quirks&lt;br&gt;
TikTok requires --impersonate chrome-131 or downloads fail. TikTok actively detects and blocks non-browser requests.&lt;br&gt;
Reddit blocks all datacenter IPs at the network level. No amount of header spoofing fixes this — it's an IP range block. Currently showing a friendly error message for Reddit URLs.&lt;br&gt;
Instagram needs a Referer: &lt;a href="https://www.instagram.com/" rel="noopener noreferrer"&gt;https://www.instagram.com/&lt;/a&gt; header or it returns 403.&lt;br&gt;
YouTube limits to 10 minutes to prevent abuse and requires specific format selection to get the best quality MP4 merge.&lt;/p&gt;

&lt;p&gt;What I'd Do Differently&lt;br&gt;
Proxy layer for Reddit — Reddit blocking is the most common complaint. A residential proxy for Reddit specifically would fix it but adds cost and complexity.&lt;br&gt;
Progress indicator — right now users just see "Processing..." with no feedback on how long it'll take. A server-sent events progress stream from yt-dlp would improve UX significantly.&lt;br&gt;
Caching popular downloads — if 50 people download the same viral TikTok in an hour, processing it 50 times is wasteful. A short TTL cache keyed by URL + format would reduce server load.&lt;/p&gt;

&lt;p&gt;Results So Far&lt;br&gt;
Launched 3 weeks ago. Currently at 180+ daily users growing consistently, driven entirely by organic Reddit posts and word of mouth. No paid ads outside of a small TikTok ad test.&lt;br&gt;
The "no ads" positioning has resonated strongly — the most common feedback is people being surprised a downloader site can actually be clean and fast.&lt;/p&gt;

&lt;p&gt;Try It&lt;br&gt;
&lt;a href="https://dltkk.to" rel="noopener noreferrer"&gt;https://dltkk.to&lt;/a&gt; — free, no signup, works on any device.&lt;br&gt;
Built with Node.js, Express, yt-dlp, and ffmpeg. Happy to answer any questions about the implementation.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>How I Handle Platform Detection and URL Validation for Multiple Video Sources in Node.js</title>
      <dc:creator>john jewski</dc:creator>
      <pubDate>Fri, 13 Feb 2026 15:31:05 +0000</pubDate>
      <link>https://forem.com/john_jewskiz/how-i-handle-platform-detection-and-url-validation-for-multiple-video-sources-in-nodejs-3ne1</link>
      <guid>https://forem.com/john_jewskiz/how-i-handle-platform-detection-and-url-validation-for-multiple-video-sources-in-nodejs-3ne1</guid>
      <description>&lt;p&gt;One of the trickiest parts of building &lt;br&gt;
dltkk.to was handling URL validation &lt;br&gt;
across TikTok, YouTube and Instagram.&lt;/p&gt;

&lt;p&gt;Each platform has completely different &lt;br&gt;
URL structures and each needs different &lt;br&gt;
validation logic. Here's how I solved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Users paste all kinds of URLs:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
https://www.tiktok.com/@user/video/123&lt;br&gt;
https://vm.tiktok.com/abc123&lt;br&gt;
https://youtube.com/watch?v=abc123&lt;br&gt;
https://youtu.be/abc123&lt;br&gt;
https://www.instagram.com/reel/abc123&lt;br&gt;
https://instagram.com/p/abc123&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;All valid. All need different handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform Detection
&lt;/h2&gt;

&lt;p&gt;First step is detecting which platform &lt;br&gt;
the URL belongs to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`javascript&lt;br&gt;
function detectPlatform(url) {&lt;br&gt;
  if (!url || typeof url !== 'string') {&lt;br&gt;
    return null&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (url.includes('tiktok.com')) {&lt;br&gt;
    return 'tiktok'&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (&lt;br&gt;
    url.includes('youtube.com/watch') || &lt;br&gt;
    url.includes('youtu.be/')&lt;br&gt;
  ) {&lt;br&gt;
    return 'youtube'&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (url.includes('instagram.com')) {&lt;br&gt;
    return 'instagram'&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return null&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Simple but effective. Returns null for &lt;br&gt;
unsupported platforms which we handle &lt;br&gt;
gracefully in the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL Normalization
&lt;/h2&gt;

&lt;p&gt;TikTok specifically has a problem with &lt;br&gt;
shortened URLs (vm.tiktok.com). These &lt;br&gt;
need to be expanded before processing.&lt;/p&gt;

&lt;p&gt;YouTube has the same issue with &lt;br&gt;
youtu.be short links.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`javascript&lt;br&gt;
function normalizeYouTubeUrl(url) {&lt;br&gt;
  const match = url.match(&lt;br&gt;
    /(?:youtube.com\/watch\?v=|youtu.be\/)([^&amp;amp;\n?#]+)/&lt;br&gt;
  )&lt;/p&gt;

&lt;p&gt;if (match) {&lt;br&gt;
    return &lt;code&gt;https://www.youtube.com/watch?v=${match[1]}&lt;/code&gt;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return url&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This extracts the video ID from any &lt;br&gt;
YouTube URL format and rebuilds a &lt;br&gt;
clean standardized URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duration Validation (YouTube Only)
&lt;/h2&gt;

&lt;p&gt;YouTube is the only platform where &lt;br&gt;
we check duration before processing. &lt;br&gt;
Long videos could hammer the server &lt;br&gt;
so we cap at 10 minutes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
function checkDuration(url) {&lt;br&gt;
  return new Promise((resolve, reject) =&amp;gt; {&lt;br&gt;
    const command =&lt;/code&gt;yt-dlp --get-duration "${url}"`&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exec(command, { timeout: 10000 }, (error, stdout) =&amp;gt; {
  if (error) {
    // Continue anyway if check fails
    resolve(true)
    return
  }

  const duration = stdout.trim()
  const parts = duration.split(':').map(Number)
  let totalSeconds = 0

  if (parts.length === 2) {
    totalSeconds = parts[0] * 60 + parts[1]
  } else if (parts.length === 3) {
    totalSeconds = parts[0] * 3600 + 
                  parts[1] * 60 + 
                  parts[2]
  }

  if (totalSeconds &amp;gt; 600) {
    reject(new Error('Video exceeds 10 minute limit'))
    return
  }

  resolve(true)
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;})&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Note: we resolve true even on error &lt;br&gt;
rather than blocking the user. The &lt;br&gt;
actual download will fail naturally &lt;br&gt;
if something is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Classification
&lt;/h2&gt;

&lt;p&gt;Different errors need different user &lt;br&gt;
facing messages:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`javascript&lt;br&gt;
function classifyError(errorOutput) {&lt;br&gt;
  if (errorOutput.includes('Sign in to confirm')) {&lt;br&gt;
    return 'YouTube requires verification. &lt;br&gt;
    Try a different video.'&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (errorOutput.includes('Unable to extract')) {&lt;br&gt;
    return 'Unable to download. &lt;br&gt;
    Check the URL and try again.'&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (errorOutput.includes('HTTP Error 429')) {&lt;br&gt;
    return 'Too many requests. &lt;br&gt;
    Wait a moment and try again.'&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (errorOutput.includes('Private video')) {&lt;br&gt;
    return 'This video is private.'&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return 'Download failed. &lt;br&gt;
  Check the URL and try again.'&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Generic errors are confusing for users. &lt;br&gt;
Classifying them makes the experience &lt;br&gt;
much better.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Cleanup
&lt;/h2&gt;

&lt;p&gt;Temporary files auto delete after &lt;br&gt;
2 minutes to prevent storage issues:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
setTimeout(() =&amp;gt; {&lt;br&gt;
  if (fs.existsSync(outputPath)) {&lt;br&gt;
    fs.unlink(outputPath, (err) =&amp;gt; {&lt;br&gt;
      if (err) {&lt;br&gt;
        console.error('Failed to delete:', err)&lt;br&gt;
      }&lt;br&gt;
    })&lt;br&gt;
  }&lt;br&gt;
}, 120000)&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Request Timeout
&lt;/h2&gt;

&lt;p&gt;Long running downloads get killed &lt;br&gt;
after 5 minutes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
const timeout = setTimeout(() =&amp;gt; {&lt;br&gt;
  process.kill()&lt;br&gt;
  res.status(500).json({ &lt;br&gt;
    error: 'Download timeout. &lt;br&gt;
    Video may be too large.' &lt;br&gt;
  })&lt;br&gt;
}, 300000)&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`javascript&lt;br&gt;
app.post('/api/download', async (req, res) =&amp;gt; {&lt;br&gt;
  const { url, format } = req.body&lt;/p&gt;

&lt;p&gt;// Detect platform&lt;br&gt;
  const platform = detectPlatform(url)&lt;br&gt;
  if (!platform) {&lt;br&gt;
    return res.status(400).json({ &lt;br&gt;
      error: 'Unsupported platform' &lt;br&gt;
    })&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;// Normalize URL&lt;br&gt;
  let videoUrl = url&lt;br&gt;
  if (platform === 'youtube') {&lt;br&gt;
    videoUrl = normalizeYouTubeUrl(url)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Check duration
try {
  await checkDuration(videoUrl)
} catch (err) {
  return res.status(400).json({ 
    error: err.message 
  })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;// Process download&lt;br&gt;
  processDownload(videoUrl, format, platform, res)&lt;br&gt;
})&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add rate limiting per IP immediately&lt;/li&gt;
&lt;li&gt;Use a queue system for heavy traffic&lt;/li&gt;
&lt;li&gt;Add Redis for caching duplicate requests&lt;/li&gt;
&lt;li&gt;Implement proper logging from day one&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try the Live Version
&lt;/h2&gt;

&lt;p&gt;Everything above is running at dltkk.to &lt;br&gt;
Free, no signup required.&lt;/p&gt;

&lt;p&gt;Questions about any part of the &lt;br&gt;
implementation? Happy to go deeper.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Free Video Downloader With Node.js - Here's How</title>
      <dc:creator>john jewski</dc:creator>
      <pubDate>Fri, 13 Feb 2026 03:32:13 +0000</pubDate>
      <link>https://forem.com/john_jewskiz/i-built-a-free-video-downloader-with-nodejs-heres-how-1bgp</link>
      <guid>https://forem.com/john_jewskiz/i-built-a-free-video-downloader-with-nodejs-heres-how-1bgp</guid>
      <description>&lt;p&gt;I recently built dltkk.to - a free browser-based &lt;br&gt;
video downloader for TikTok, YouTube and Instagram.&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%2Fuabx8d8m86lq7ewx5xid.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%2Fuabx8d8m86lq7ewx5xid.png" alt=" " width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the tech stack and what I learned building it.&lt;/p&gt;

&lt;p&gt;What it does&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloads TikTok videos without watermark&lt;/li&gt;
&lt;li&gt;Converts YouTube videos to MP3&lt;/li&gt;
&lt;li&gt;Saves Instagram Reels&lt;/li&gt;
&lt;li&gt;Browser based (no app installation)&lt;/li&gt;
&lt;li&gt;Completely free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tech Stack&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Node.js + Express&lt;br&gt;
&lt;strong&gt;Video Processing:&lt;/strong&gt; Custom built processing engine&lt;br&gt;
&lt;strong&gt;Server:&lt;/strong&gt; Linux VPS&lt;br&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; HTML, CSS, Tailwind&lt;/p&gt;

&lt;p&gt;How it works&lt;/p&gt;

&lt;p&gt;User pastes a TikTok/YouTube/Instagram URL into &lt;br&gt;
the input box. The Node.js backend receives the &lt;br&gt;
URL, validates the platform, then passes it to &lt;br&gt;
my custom video processing engine.&lt;/p&gt;

&lt;p&gt;The processing engine handles each platform &lt;br&gt;
differently. For TikTok it strips the watermark &lt;br&gt;
during the download process. For YouTube it &lt;br&gt;
extracts and converts the audio stream to MP3.&lt;/p&gt;

&lt;p&gt;Files are temporarily stored on the server and &lt;br&gt;
automatically deleted after 2 minutes to save &lt;br&gt;
storage space.&lt;/p&gt;

&lt;p&gt;Building the Processing Engine&lt;/p&gt;

&lt;p&gt;This was the hardest part of the project. &lt;br&gt;
Each platform has different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL structures&lt;/li&gt;
&lt;li&gt;Authentication requirements
&lt;/li&gt;
&lt;li&gt;Video stream formats&lt;/li&gt;
&lt;li&gt;Rate limiting behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Had to reverse engineer how each platform &lt;br&gt;
serves video content and build handlers &lt;br&gt;
for each one separately.&lt;/p&gt;

&lt;p&gt;What I learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Always validate URLs server side not just client&lt;/li&gt;
&lt;li&gt;Set timeouts on downloads (5 min max)&lt;/li&gt;
&lt;li&gt;Auto delete temp files (learned this the hard way)&lt;/li&gt;
&lt;li&gt;Each platform needs completely different 
handling logic&lt;/li&gt;
&lt;li&gt;User agent spoofing is necessary to avoid 
being blocked&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The YouTube Problem&lt;/p&gt;

&lt;p&gt;YouTube was by far the hardest platform. &lt;br&gt;
They actively fight against third party &lt;br&gt;
downloading tools. Had to build in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Format selection logic&lt;/li&gt;
&lt;li&gt;Video and audio stream merging&lt;/li&gt;
&lt;li&gt;Age restriction handling&lt;/li&gt;
&lt;li&gt;Rate limit detection and backoff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it&lt;/p&gt;

&lt;p&gt;Check it out at dltkk.to&lt;/p&gt;

&lt;p&gt;Feedback welcome especially on the Node.js &lt;br&gt;
backend architecture!&lt;/p&gt;

</description>
      <category>node</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
