Without FastDL, players downloading a 40 MB custom map over the game socket at ~20 KB/s wait 33 minutes before they can join. With FastDL on the same VPS, that download takes 8 seconds. This guide walks through the full production setup on Ubuntu 22.04/24.04 — nginx HTTPS vhost, Let's Encrypt SSL, bz2 compression for 60–80% bandwidth savings, automated rsync via systemd timers, and a migration path to Cloudflare R2 or BunnyCDN when your server takes off.

Who this is for

TF2 community server operators, CS2 admins running custom maps or workshop collections, and anyone running a Source-engine server where sv_allowdownload 1 is not cutting it. If all your players connect on <50 ms LAN, you may not need this. Everyone else does.

01 // Requirements

  • A VPS with a public IPv4 — the game server and the FastDL web server can live on the same box to start. Split them later if bandwidth becomes an issue.
  • A domain you control — you need to create a subdomain like fastdl.example.com. Free Cloudflare DNS is fine.
  • Ubuntu 22.04 or 24.04 LTS — commands below target both. Debian 12 works identically.
  • Port 80 and 443 open — required for HTTP and HTTPS. Also required for Let's Encrypt HTTP-01 challenge.
  • Root or sudo access — nginx and certbot both need elevated privileges during install.
  • ~1 GB extra disk — FastDL just needs space for your custom maps and assets plus their bz2 copies.
Before you start — the 60-second decision

Running a vanilla CS2 server with no custom maps and pulling everything from the Steam Workshop? You probably do not need FastDL at all — Steam Workshop handles map distribution via Steam's own CDN.

Running a TF2 community server, a CSS/CSGO Legacy server, or CS2 with custom workshop maps that are large and frequently changed? You need FastDL. Skip this guide at your players' peril.

Running a popular community server with 50+ concurrent downloads at peak? Start with the local FastDL in this guide, then migrate to the CDN setup in section 14 once you have real player data.

Recommended Hosting — Game Server + FastDL on One Box

Hostinger KVM 2 — 2 vCPU, 8 GB RAM, 100 GB NVMe

A single KVM 2 runs a 24-slot TF2 or CS2 server plus nginx + FastDL comfortably. NVMe disk means your bz2 compressed assets read fast under download pressure, and 200 MBit/s unmetered bandwidth is enough for most community servers before you need a CDN. If you are running multiple servers or expect 100+ concurrent map downloads, step up to KVM 4.

Get Hostinger KVM 2 →

02 // What is FastDL (and why the default is so bad)

When a player joins a Source-engine server that requires a map or asset they do not have locally, the game has two ways to download it:

  • Over the game socket (the slow way) — the engine throttles map and asset downloads to roughly 20–30 KB/s over the UDP game port. This is deliberate: Valve did not want a huge download to starve the rest of the tick rate. On a 40 MB map, that is 22–33 minutes of staring at a loading screen while other players play.
  • Over HTTP/HTTPS via sv_downloadurl (FastDL) — the engine redirects clients to a plain web server to pull the same file. Downloads are limited only by the web server's and player's connection speed. On the same 40 MB map over a 10 MBit/s consumer connection, the player waits ~32 seconds. On gigabit fibre: under 5 seconds.

The math is always in FastDL's favour. A community server without FastDL bleeds players on every map change — 30% to 60% of joiners simply give up during the download. With FastDL configured correctly, that drop-off disappears.

Why HTTPS (not HTTP) for 2026

Source engine has supported HTTPS sv_downloadurl since 2015, and modern clients complain about plain-HTTP URLs in console logs. Browsers and antivirus tools also flag HTTP downloads. There is no reason to serve FastDL over plain HTTP in 2026 — Let's Encrypt is free and this guide uses HTTPS everywhere.

03 // DNS & subdomain setup

Decide your FastDL hostname. Good choices:

  • fastdl.example.com — clearest, most conventional.
  • dl.example.com — short, fine.
  • cdn.example.com — aspirational but reasonable.

Avoid example.com root and www.example.com — you will want those for your actual website later. Keep FastDL on its own subdomain so you can move it to a CDN or a separate VPS without breaking your main site.

In your DNS panel (Cloudflare, Namecheap, Hostinger DNS, wherever your domain lives), create a new A record:

Type:  A
Name:  fastdl           # produces fastdl.example.com
Value: 203.0.113.42     # your VPS public IPv4
TTL:   3600             # 1 hour is fine, lower to 300 while testing
Proxy: OFF (greyed out) # if using Cloudflare, leave DNS-only for now
Cloudflare users — keep the proxy OFF initially

Let's Encrypt's HTTP-01 challenge cannot complete through the Cloudflare proxy in certain configurations. Leave the orange cloud off (DNS-only) until the certificate is issued. You can turn the proxy on afterwards, and doing so gives you free DDoS protection on your FastDL host — but note that Cloudflare's free plan does not allow arbitrary binary file serving at scale. See section 14 for a better CDN path.

Verify the DNS has propagated before continuing:

# From your local machine or the VPS itself
dig +short fastdl.example.com
# Expect: 203.0.113.42 (your VPS IP)

# Or with nslookup
nslookup fastdl.example.com 1.1.1.1

If the DNS does not resolve yet, wait 5 minutes and try again. DNS propagation with TTL 3600 usually completes within a few minutes but can occasionally take up to an hour.

04 // Install nginx

We use nginx rather than Apache for three reasons: lower memory footprint (important on a 4–8 GB VPS already running a game server), simpler vhost syntax for a static-file server, and better performance under the concurrent-download patterns FastDL generates.

# Update package lists and install nginx
sudo apt update
sudo apt install -y nginx

# Enable and start nginx
sudo systemctl enable --now nginx

# Verify it responds on port 80
curl -I http://localhost
# HTTP/1.1 200 OK
# Server: nginx/1.24.0

Also open ports 80 and 443 in your firewall:

# If using UFW (default on Ubuntu Server)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload

# Check status
sudo ufw status

If your VPS provider also has an external firewall (Hostinger, Hetzner Cloud, AWS EC2, etc.), open 80 and 443 there too.

05 // Create the nginx vhost

Create the FastDL document root and give it sane ownership:

sudo mkdir -p /var/www/fastdl
sudo chown -R www-data:www-data /var/www/fastdl
sudo chmod 755 /var/www/fastdl

Now create the vhost file. Start with HTTP-only — we will upgrade to HTTPS with certbot in the next section, and certbot needs HTTP working first to complete the challenge.

sudo nano /etc/nginx/sites-available/fastdl

Paste this config, replacing fastdl.example.com with your actual subdomain:

server {
    listen 80;
    listen [::]:80;
    server_name fastdl.example.com;

    root /var/www/fastdl;
    index index.html;

    # Disable directory listing — FastDL should be opaque
    autoindex off;

    # Allow .bsp, .bz2, .vmt, .vtf, .mdl, .phy, .vvd, .dx80, .dx90, .sw, .wav, .mp3
    location ~* \.(bsp|bz2|vmt|vtf|mdl|phy|vvd|dx80|dx90|sw|wav|mp3|nav|res|pcf|txt)$ {
        # Cache aggressively — content is immutable per filename
        expires 30d;
        add_header Cache-Control "public, immutable";

        # Disable access log spam from thousands of map downloads
        access_log off;

        # Log denied access attempts only
        log_not_found off;
    }

    # Deny everything else — no .htaccess, no .git, no PHP
    location / {
        try_files $uri $uri/ =404;
    }

    location ~ /\. {
        deny all;
    }

    # Keep access log small — just errors
    error_log /var/log/nginx/fastdl_error.log warn;
}

Enable the vhost and reload nginx:

sudo ln -s /etc/nginx/sites-available/fastdl /etc/nginx/sites-enabled/

# Remove the default vhost if it will conflict
sudo rm -f /etc/nginx/sites-enabled/default

# Test the config syntax
sudo nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# Reload
sudo systemctl reload nginx

Verify from another machine:

curl -I http://fastdl.example.com/
# HTTP/1.1 404 Not Found    ← this is correct, the directory is empty
# Server: nginx/1.24.0

A 404 here is the correct response — the directory is empty and autoindex is off. What matters is that nginx is answering on your domain.

06 // Let's Encrypt SSL with certbot

Certbot is the canonical Let's Encrypt client and ships with an nginx plugin that edits your vhost for you and handles automatic renewal.

sudo apt install -y certbot python3-certbot-nginx

# Issue the certificate — certbot edits /etc/nginx/sites-available/fastdl automatically
sudo certbot --nginx -d fastdl.example.com \
    --agree-tos --no-eff-email -m you@example.com

Certbot will ask whether to redirect HTTP to HTTPS. Choose option 2 (Redirect) — this ensures all FastDL traffic goes over HTTPS, which is what you want.

Verify the certificate:

curl -I https://fastdl.example.com/
# HTTP/2 404
# Server: nginx/1.24.0

# Verify auto-renewal will work
sudo certbot renew --dry-run
# Congratulations, all simulated renewals succeeded

Certbot installs a systemd timer (certbot.timer) that checks renewal twice daily. Certificates renew automatically 30 days before expiry.

07 // Configure sv_downloadurl in server.cfg

In your CS2 or TF2 server config (typically cs2/game/csgo/cfg/server.cfg or tf/cfg/server.cfg), add these lines:

// --- FastDL configuration ---
sv_allowdownload         1
sv_allowupload           0
sv_downloadurl           "https://fastdl.example.com/"
net_maxfilesize          64
sv_consistency           1

Explanation of each line:

  • sv_allowdownload 1 — permits clients to download from this server. Required.
  • sv_allowupload 0 — blocks clients from uploading files to your server. Always 0 in production.
  • sv_downloadurl — the URL where the client should fetch files. Must end with a trailing slash. The client appends the relative path to this base.
  • net_maxfilesize 64 — maximum single-file size in MB. Bump to 128 if you have huge custom maps.
  • sv_consistency 1 — kicks players whose downloaded files do not match the server's checksums. Leave on.
Don't want to hand-write the whole server.cfg?

Drop these five FastDL lines into a full server.cfg using our CS2 / TF2 Server Config Generator — 174 documented cvars with inline help, official Valve game-mode presets (Competitive, Wingman, Retakes, Deathmatch), MatchZy support, and a one-click download. Free, browser-only, no signup.

Trailing slash matters

The engine does sv_downloadurl + relative_path. If you set sv_downloadurl "https://fastdl.example.com" without the trailing slash, you get https://fastdl.example.commaps/mymap.bsp.bz2 which obviously 404s. Always include the trailing slash.

Restart the game server for the changes to take effect:

# If managed via systemd
sudo systemctl restart tf2-server
# Or manually: shut down and relaunch srcds_run

08 // Mirror the game server assets

FastDL works by serving an exact mirror of the game server's file layout, rooted at whatever directory the game expects. For TF2 and CS2 the relevant directories are:

  • maps/.bsp custom maps (the big bandwidth consumer)
  • materials/.vmt and .vtf textures used by custom maps or skins
  • models/.mdl, .phy, .vvd, .dx80.vtx, .dx90.vtx, .sw.vtx for custom models and props
  • sound/.wav and .mp3 custom sounds
  • particles/.pcf custom particle systems (TF2-heavy)
  • resource/ — custom HUD files, closecaption_*.txt localisation, etc.

The client requests files by exact relative path. If the server loads maps/cp_myawesome.bsp from /srv/tf2/tf/maps/cp_myawesome.bsp, the client will fetch https://fastdl.example.com/maps/cp_myawesome.bsp.bz2 from your FastDL host — and the server's mirror must contain exactly that file at exactly that relative path.

Set up the initial mirror. Adjust the source paths for your own install:

# For TF2 (srcds installed at /srv/tf2)
GAMEDIR=/srv/tf2/tf
FASTDL=/var/www/fastdl

sudo mkdir -p $FASTDL/{maps,materials,models,sound,particles,resource}
sudo rsync -av --include='*/' \
    --include='*.bsp' --include='*.vmt' --include='*.vtf' \
    --include='*.mdl' --include='*.phy' --include='*.vvd' \
    --include='*.vtx' --include='*.wav' --include='*.mp3' \
    --include='*.pcf' --include='*.res' --include='*.txt' \
    --exclude='*' \
    $GAMEDIR/maps/   $FASTDL/maps/

sudo rsync -av --include='*/' \
    --include='*.vmt' --include='*.vtf' \
    --exclude='*' \
    $GAMEDIR/materials/ $FASTDL/materials/

# Repeat pattern for models/, sound/, particles/, resource/ as needed
Only mirror what players need to download

You do not need to mirror Valve's stock maps — every TF2 or CS2 client already has cp_badlands.bsp, de_dust2.bsp, and friends. Mirror only custom maps and assets. For TF2, the stock maps ship with the client install; for CS2, the same is true for official maps. Mirroring stock maps wastes disk and bandwidth for no gain.

09 // bz2 compression for 60–80% bandwidth savings

The Source engine supports (and expects) bz2-compressed assets on FastDL hosts. When a client needs cp_myawesome.bsp, it first requests cp_myawesome.bsp.bz2 and decompresses locally. Only if that fails does it fall back to the uncompressed .bsp.

BSP files compress exceptionally well — typical savings are 60% to 80% depending on map complexity. A 40 MB map becomes a 10–15 MB bz2. On a busy server, bz2 compression is the single biggest bandwidth-saving you can make.

Install bzip2 and write a helper script that compresses every relevant file in the FastDL tree:

sudo apt install -y bzip2

sudo tee /usr/local/bin/fastdl-compress > /dev/null << 'SCRIPT'
#!/usr/bin/env bash
# fastdl-compress — compress every .bsp and custom asset in /var/www/fastdl
# with bzip2 -k (keep original). Idempotent: skips files whose .bz2 is newer.

set -euo pipefail
FASTDL="${FASTDL:-/var/www/fastdl}"
PATTERNS=( '*.bsp' '*.vmt' '*.vtf' '*.mdl' '*.phy' '*.vvd' '*.vtx' '*.wav' '*.mp3' '*.pcf' )

cd "$FASTDL"
for pat in "${PATTERNS[@]}"; do
    while IFS= read -r -d '' f; do
        bz2="${f}.bz2"
        if [[ ! -f "$bz2" || "$f" -nt "$bz2" ]]; then
            bzip2 -kf -9 "$f"
            chown www-data:www-data "$bz2"
        fi
    done < <(find . -type f -name "$pat" -print0)
done
echo "[fastdl-compress] done at $(date -Iseconds)"
SCRIPT

sudo chmod +x /usr/local/bin/fastdl-compress

# Run it once manually to compress existing assets
sudo /usr/local/bin/fastdl-compress

After the first run, every .bsp in /var/www/fastdl/maps/ will have a .bsp.bz2 sibling. The script is idempotent — running it again only compresses files that have changed.

Why -9 and not default

bzip2 -9 is the maximum compression level. It is ~40% slower to compress than default (-6) but produces 2–5% smaller files and is — crucially — no slower to decompress on the client. You compress once, every client decompresses — optimise for the client side. Use -9.

10 // Permissions — the #1 silent failure

More FastDL setups break because of file permissions than any other single cause. The problem: your game server writes new maps as the srcds user, but nginx reads them as www-data. If the game server dumps a new custom map into /srv/tf2/tf/maps/, then your sync script rsyncs it into /var/www/fastdl/maps/, the file may land with mode 600 (owner read-only) and nginx will serve 403 Forbidden to every client.

Fix the initial state:

# Recursively set correct ownership and modes on the FastDL tree
sudo chown -R www-data:www-data /var/www/fastdl
sudo find /var/www/fastdl -type d -exec chmod 755 {} +
sudo find /var/www/fastdl -type f -exec chmod 644 {} +

Make it stay fixed. Add --chmod and --chown to your rsync in the sync script (shown in the next section) and add a small umask fix to the srcds service or your sync script:

# Inside the sync script, after rsync:
find /var/www/fastdl -type d -exec chmod 755 {} +
find /var/www/fastdl -type f -exec chmod 644 {} +
chown -R www-data:www-data /var/www/fastdl

This is fast even on large trees and guarantees permissions stay correct every time the sync runs.

11 // Automated sync with systemd timers

Manually rsyncing every time you add a new map is a recipe for missed deploys. The production pattern is a small sync script driven by a systemd timer that runs every 15 minutes.

Write the sync script:

sudo nano /usr/local/bin/fastdl-sync

Paste:

#!/usr/bin/env bash
# fastdl-sync — mirror game server assets to FastDL document root,
# fix permissions, compress new files with bz2. Idempotent.

set -euo pipefail

GAMEDIR="${GAMEDIR:-/srv/tf2/tf}"
FASTDL="${FASTDL:-/var/www/fastdl}"
LOG="/var/log/fastdl-sync.log"

{
    echo "[$(date -Iseconds)] sync start"

    for sub in maps materials models sound particles resource; do
        [[ -d "$GAMEDIR/$sub" ]] || continue
        mkdir -p "$FASTDL/$sub"
        rsync -a --delete \
            --include='*/' \
            --include='*.bsp' --include='*.vmt' --include='*.vtf' \
            --include='*.mdl' --include='*.phy' --include='*.vvd' \
            --include='*.vtx' --include='*.wav' --include='*.mp3' \
            --include='*.pcf' --include='*.res' --include='*.txt' \
            --exclude='*' \
            "$GAMEDIR/$sub/" "$FASTDL/$sub/"
    done

    FASTDL="$FASTDL" /usr/local/bin/fastdl-compress

    chown -R www-data:www-data "$FASTDL"
    find "$FASTDL" -type d -exec chmod 755 {} +
    find "$FASTDL" -type f -exec chmod 644 {} +

    echo "[$(date -Iseconds)] sync done"
} >> "$LOG" 2>&1

Make it executable:

sudo chmod +x /usr/local/bin/fastdl-sync

Create the systemd service unit at /etc/systemd/system/fastdl-sync.service:

[Unit]
Description=Sync game server assets to FastDL
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/fastdl-sync
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

And the matching timer at /etc/systemd/system/fastdl-sync.timer:

[Unit]
Description=Run FastDL sync every 15 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
AccuracySec=30s
Persistent=true

[Install]
WantedBy=timers.target

Enable and start the timer:

sudo systemctl daemon-reload
sudo systemctl enable --now fastdl-sync.timer
sudo systemctl start fastdl-sync.service
sudo systemctl status fastdl-sync.timer
sudo journalctl -u fastdl-sync.service --since '5 min ago'
Why systemd timers not cron

Systemd timers give you Persistent=true (runs missed runs after reboot), IOSchedulingClass=best-effort so the sync does not interrupt the game server, and journalctl integration for free. On modern Ubuntu, reach for timers.

12 // Verification — HTTP and in-game

Before declaring victory, verify FastDL works at two levels: from a shell with curl, and from a real game client.

HTTP verification — pick any custom map on your server and confirm the bz2 is served correctly:

# Should return HTTP 200 and a Content-Length matching the file size
curl -I https://fastdl.example.com/maps/cp_myawesome.bsp.bz2
# HTTP/2 200
# content-length: 12834562
# content-type: application/octet-stream
# cache-control: public, immutable

# Download a sample byte range to confirm the content is valid bz2
curl -s -r 0-9 https://fastdl.example.com/maps/cp_myawesome.bsp.bz2 | xxd
# 00000000: 425a 6839 3141 5926 5359 ...   <- "BZh9" magic header

A successful curl with the BZh magic bytes (0x42 0x5A 0x68) confirms bz2 is intact and nginx is serving it with the correct Content-Type.

In-game verification — the definitive test:

  1. Delete the map locally on a test client (Steam/steamapps/common/Team Fortress 2/tf/maps/cp_myawesome.bsp).
  2. Join your server. The client should show "Receiving https://fastdl.example.com/maps/cp_myawesome.bsp.bz2" at megabytes per second, not "Receiving from server" at 20 KB/s.
  3. Tail the nginx access log to confirm the hit actually came from FastDL: sudo tail -f /var/log/nginx/access.log.

If the client still downloads at 20 KB/s, the engine could not reach the FastDL URL and fell back to the game socket. Check section 16 — troubleshooting — for every known cause.

13 // CS2-specific notes & the Workshop

CS2 is a different animal from TF2 and CS:GO Legacy when it comes to FastDL.

  • CS2 official maps ship with every client install — any map in Valve's official pool (de_mirage, de_inferno, etc.) is already on every player's disk. Never mirror these to FastDL.
  • CS2 Workshop maps come via Steam's CDN, not FastDL — when a player joins a server running a workshop map they do not have, the CS2 client auto-subscribes and pulls the .vpk from Steam's CDN. This bypasses sv_downloadurl entirely and is faster than FastDL in most cases.
  • Where FastDL still matters for CS2 — server-specific custom content that is not in the workshop: custom HUD files, admin-panel resources, bespoke sprays, and — most commonly — if you run a private or competitive pool where the workshop route is awkward.
  • host_workshop_collection — a CS2 server can advertise a workshop collection ID and the client downloads everything in the collection on connect. This is the Valve-recommended path for CS2 custom-map servers and removes the need for FastDL for those maps.
TF2 is where FastDL really earns its keep

TF2 has no Steam Workshop integration comparable to CS2 for server-side map distribution. TF2 community servers absolutely depend on FastDL for every custom map, every HUD asset, every custom sound. If your server is TF2, do not skip this guide.

14 // Bandwidth scaling — when to move off your VPS

A single Hostinger KVM 2 with ~200 Mbit/s unmetered bandwidth can comfortably serve 25–50 concurrent map downloads at reasonable speeds. Beyond that, players start queueing and join times climb.

Rough thresholds based on typical community-server traffic:

  • < 10 avg concurrent joiners — local FastDL on the same VPS as the game server is fine. Do not over-engineer.
  • 10–50 avg concurrent joiners — move FastDL to a second Hostinger KVM 1. Dedicated bandwidth, isolates web traffic from game traffic.
  • 50+ concurrent joiners OR multi-region audience — migrate to a CDN (next section). The per-GB cost drops dramatically and latency improves for players outside your VPS's region.

Monitor your FastDL bandwidth usage. Install vnstat and check daily totals:

sudo apt install -y vnstat

# Wait 24h for data, then
vnstat -d   # daily breakdown
vnstat -m   # monthly

If you are pushing 200 GB/month or more through FastDL on a single VPS, the CDN migration in the next section will pay for itself in reduced player complaints alone.

15 // Migrating to Cloudflare R2 or BunnyCDN

A CDN gives you two things a single-VPS FastDL cannot: geographic distribution (edge nodes near the player) and burst capacity (a thousand concurrent downloads do not slow each other down). For a busy TF2 or CS2 server, the math usually favours a CDN once you cross ~100 GB/month of FastDL traffic.

Two options both work well for Source engine FastDL:

Cloudflare R2 (recommended for most)

  • Zero egress fees — R2 does not charge for bandwidth out, only storage and operations. For a FastDL workload (read-heavy, predictable) this is the cheapest option on the market.
  • Storage: $0.015/GB/month. 10 GB of custom maps = $0.15/month.
  • Class A operations (writes): $4.50 per million. You will never approach this.
  • Class B operations (reads): $0.36 per million. A busy server with 50k downloads/month = $0.02.
  • Setup: create an R2 bucket, enable the custom subdomain, point sv_downloadurl at the bucket URL. Upload via rclone from your VPS sync script.

BunnyCDN (simpler, per-GB)

  • Pay-per-GB egress: $0.005–$0.04/GB depending on region. Europe and North America are $0.01/GB.
  • Storage: $0.01/GB/month.
  • Setup friction is lower than R2 — Bunny's dashboard is more game-server-friendly.
  • Good choice if your player base is regionally concentrated and you want predictable latency without wrestling with Cloudflare Workers.

Migrating your sync script to push to R2 is straightforward. Install rclone, configure an R2 remote, and replace the local permission/compression step with an upload step:

sudo apt install -y rclone
rclone config   # interactive — choose s3, provider Cloudflare, enter R2 credentials

# In fastdl-sync, after compression:
rclone sync /var/www/fastdl r2:my-fastdl-bucket \
    --include '*.bz2' --include '*.bsp' \
    --checksum --fast-list --transfers 8

Point sv_downloadurl at the R2 public URL (e.g. https://pub-xxxxx.r2.dev/ or your custom CNAME) and you are done. nginx on the VPS becomes optional at that point — the CDN handles all downloads.

Keep nginx as a fallback

Even after migrating to a CDN, it is worth keeping the local nginx FastDL configured and synced. If the CDN has a regional outage, you can flip sv_downloadurl back to your VPS in 30 seconds and players keep playing. Costs nothing extra to keep running.

Recommended VPS for FastDL + Game Server

Hostinger KVM 2 — 2 vCPU, 8 GB RAM, 100 GB NVMe

Enough headroom to run both a TF2 or CS2 server and the FastDL nginx on the same box for servers under 32 players. ~200 Mbit/s unmetered bandwidth included — plenty for 25–50 concurrent downloads. €8.49/month on the annual plan.

Get Hostinger KVM 2 →
For 50+ Player Community Servers

Hostinger KVM 4 — 4 vCPU, 16 GB RAM, 200 GB NVMe

If you run a large community server with a heavy SourceMod plugin stack and a big custom-map rotation, KVM 4 gives you the CPU headroom to run both game and FastDL without contention. €12.99/month.

Get Hostinger KVM 4 →

16 // Troubleshooting — 16 failure modes

Every failure mode below has bitten me or a reader at some point. In rough order of frequency:

SymptomCauseFix
Client downloads at 20 KB/s from the game socket, not FastDLsv_downloadurl unreachable, malformed, or returning non-200curl -I <url> from your local machine. Must return 200 for the exact file. Trailing slash on URL.
403 Forbidden on every fileFile permissions — nginx www-data cannot readsudo chown -R www-data:www-data /var/www/fastdl && sudo find /var/www/fastdl -type f -exec chmod 644 {} +
404 Not Found on .bsp.bz2 but file existsCase sensitivity — Linux is case-sensitive, Windows client may send different caseRename all FastDL filenames to lowercase. Convention: all map files lowercase.
Client gets "missing map" error despite download appearing to succeedbz2 downloaded but decompression failed — usually corrupt bz2 or wrong Content-TypeDelete and recompress: rm file.bsp.bz2 && bzip2 -kf -9 file.bsp. Verify with file file.bsp.bz2 (should say "bzip2 compressed data").
Download starts but stalls at random percentagesnginx client_max_body_size or reverse-proxy timeout too lowIn the server {} block: client_max_body_size 0; (disable the limit for FastDL).
SSL certificate errors in client consoleSelf-signed or expired cert — Source engine refuses themUse Let's Encrypt via certbot (section 06). Never use self-signed for FastDL.
Certbot renewal silently failsPort 80 closed after going all-HTTPS; certbot's HTTP-01 challenge needs port 80 openKeep port 80 open. The nginx vhost still needs a listen 80 block that certbot can use for challenges. Do not firewall port 80.
Downloads work on old clients, fail on new onesHTTP/2 incompatibility with very old Source engine buildsRare on TF2/CS2. If it happens, force HTTP/1.1 in nginx: remove http2 from the listen directive.
Player reports "downloading but server disconnects before finish"Game server net_maxfilesize too low for the map being downloadedRaise net_maxfilesize to 128 or higher in server.cfg. Default 16 is too low for modern maps.
Sync script runs but files never appear on FastDLWrong GAMEDIR path in script, or rsync include/exclude rules filtering too aggressivelyRun fastdl-sync manually with bash -x to trace. Confirm GAMEDIR matches actual game install.
bz2 files grow but disk fills upSync keeps old files because rsync lacks --delete; stale bz2s accumulateAdd --delete to the rsync invocations in the sync script (already shown in section 11).
Nginx serves files but with Content-Type: text/plainDefault MIME type fallback — .bz2 not in mime.typesAdd to the vhost: types { application/octet-stream bz2; } inside the location ~* \.bz2$ block.
Player downloads the map but server still rejects: "missing file"sv_consistency 1 + checksum mismatch — your FastDL bz2 was compressed from a different build of the map than the server has loadedRecompress the bz2 from the exact same .bsp the server is running. The sync script handles this correctly; manual mismatches cause this.
Cloudflare in front of FastDL: random 502 errorsCloudflare's 100MB free-plan upload limit is irrelevant here, but request timeout (100s) can hit on huge maps over slow connectionsEither upgrade Cloudflare plan, bypass Cloudflare for FastDL subdomain (grey cloud), or move to R2 (section 15).
Works from your IP, fails from player IPsGeoIP firewall rule or fail2ban blocking foreign traffic on port 443Check sudo fail2ban-client status nginx-* and sudo iptables -L -n | grep DROP. FastDL must be reachable worldwide.
Everything worked then stopped after a game updateGame server restart changed umask or file ownership, and your sync has not run yetManually run sudo systemctl start fastdl-sync.service and wait 30s. Shorten timer interval if this happens often.

17 // Next Steps

  • CS2 server setupFull CS2 dedicated server guide for Linux if you have not set up the game server itself yet.
  • TF2 community serverTF2 community server + SourceMod guide for the plugin stack most community servers need.
  • Harden the VPS — basic SSH key auth, UFW firewall, and fail2ban. Standard Linux hygiene applies to your FastDL host just like any other public server.
  • Monitor bandwidth — check vnstat -m monthly. When you hit the thresholds in section 14, migrate to a CDN before players notice.
  • Backup your vhost & sync config — the FastDL content can always be regenerated from the game server, but losing your nginx vhost or sync script is a real pain. /etc/nginx/ and /usr/local/bin/fastdl-* belong in your backups.

18 // Frequently Asked Questions

What are the benefits of FastDL?

FastDL bypasses Source engine's painfully slow built-in file transfer (which caps at 20KB/s and chokes when multiple players join at once). With FastDL, players download custom maps, models, and sounds from a separate HTTP server at full bandwidth — typically 50-100x faster. On a 50MB custom map, this is the difference between a 40-minute connect and a 10-second connect. For community servers running custom content, FastDL is not optional — it's the difference between players staying and ragequitting at the loading screen.

What games use FastDL?

Any Source engine game that allows custom content downloads: Counter-Strike 2, Team Fortress 2, Counter-Strike: Source, Counter-Strike 1.6, Garry's Mod, Left 4 Dead 2, Day of Defeat, Half-Life 2: Deathmatch, and No More Room in Hell. The FastDL protocol is part of the engine itself. Note that CS2 has slightly different content-pathing rules vs CS:GO — our guide covers the CS2-specific quirks like the materials/ and maps/ mirror layout.

Is FastDL safe to set up?

Yes, if you do it correctly. FastDL serves static files over HTTP from a web server you control — there's no code execution, no client-side injection, no security surface beyond what any web host already has. The two real risks: (1) accidentally exposing your entire server filesystem if you point the web root at the wrong directory, and (2) serving uncompressed files which wastes 60-80% bandwidth. Our guide walks through bzip2-compressing your assets and locking the web root to a dedicated fastdl/ directory.

Can I run FastDL on the same machine as my game server?

Yes, but with caveats. Install nginx or apache on the same box, point it at your fastdl/ folder, and set sv_downloadurl to http://yourserverip/fastdl. This works fine for small communities (under 20 concurrent connects). For larger servers, run FastDL on a separate VPS or use Cloudflare R2 with public access — download bandwidth shouldn't compete with game-tick CPU. A cheap $3-5/mo VPS handles thousands of concurrent FastDL downloads easily.