← Back to Blog Apr 23, 2026 · 04:00 PM · 5 min read

Snake Speed Increase: Get Faster as You Grow

A constant-speed Snake game gets boring fast. In Part 2, we added score tracking and high scores. Now the real challenge comes from the game speeding up as you grow — each food eaten makes the next one harder to reach. In this part, we replace the fixed interval with dynamic speed that scales with the snake's length.

By Wuidi · Tutorial · Part 3

What You Will Learn

Step 1: Base Speed and Formula

Instead of a fixed setInterval, we calculate the interval dynamically based on how many food the snake has eaten. The formula starts slow and gets faster with each food.

const BASE_SPEED = 150;    // starting interval (ms)
const MIN_SPEED = 60;      // fastest possible (ms)
const SPEED_STEP = 5;      // ms reduction per food

function getSpeed() {
  const foodEaten = snake.length - 3; // started with 3 segments
  const speed = BASE_SPEED - (foodEaten * SPEED_STEP);
  return Math.max(speed, MIN_SPEED);
}

The snake starts with 3 segments, so foodEaten is the current length minus 3. Each food reduces the interval by 5ms. At 150ms base and 5ms per food, the snake reaches max speed after 18 food items: 150 - (18 × 5) = 60ms.

Tip: The Math.max() call is critical. Without it, the interval could go to zero or negative, which would freeze or crash the game. Always cap your speed.

Step 2: requestAnimationFrame with Timestamps

A fixed setInterval cannot change its interval after creation. We switch to requestAnimationFrame with timestamp tracking, which lets us change the tick rate every frame.

let lastTick = 0;

function gameLoop(timestamp) {
  if (gameOver) return;

  const elapsed = timestamp - lastTick;
  const currentSpeed = getSpeed();

  if (elapsed >= currentSpeed) {
    lastTick = timestamp;

    moveSnake();
    checkFood();
    updateFloatingTexts();

    if (checkCollision()) {
      gameOver = true;
      drawGameOver();
      return;
    }

    draw();
    drawFloatingTexts();
    drawScore();
  }

  requestAnimationFrame(gameLoop);
}

// Start the loop
requestAnimationFrame(gameLoop);

The browser calls gameLoop roughly 60 times per second. We only process a game tick when enough time has passed (based on getSpeed()). This gives us smooth, variable-speed gameplay without creating and destroying intervals.

Key insight: requestAnimationFrame passes a high-resolution timestamp as the first argument. We compare it against lastTick to decide when to process the next game step. This is more reliable than setInterval for variable timing.

Step 3: Speed Tiers

Instead of a smooth linear increase, you can use discrete speed tiers. Every 5 food items, the speed jumps to the next tier. This creates noticeable "level up" moments that feel more rewarding.

const SPEED_TIERS = [
  { threshold: 0,  speed: 150 },  // Tier 1: start
  { threshold: 5,  speed: 130 },  // Tier 2: getting warm
  { threshold: 10, speed: 110 },  // Tier 3: picking up
  { threshold: 15, speed: 90 },   // Tier 4: fast
  { threshold: 20, speed: 75 },   // Tier 5: very fast
  { threshold: 25, speed: 60 },   // Tier 6: max speed
];

function getSpeedTier() {
  const foodEaten = snake.length - 3;
  let currentTier = SPEED_TIERS[0];

  for (const tier of SPEED_TIERS) {
    if (foodEaten >= tier.threshold) {
      currentTier = tier;
    }
  }

  return currentTier;
}

function getSpeed() {
  return getSpeedTier().speed;
}

The tier system is easier to tune than a formula. You can adjust each tier independently — maybe the jump from Tier 3 to Tier 4 is too harsh, so you change it from 90 to 100. With a formula, changing one point affects the entire curve.

Step 4: Visual Speed Indicator

Show the player which speed tier they are on. A small bar or label in the corner gives feedback that the game is getting harder.

function drawSpeedIndicator() {
  const tier = getSpeedTier();
  const tierIndex = SPEED_TIERS.indexOf(tier);
  const tierCount = SPEED_TIERS.length;

  // Background bar
  const barX = 8;
  const barY = HEIGHT - 16;
  const barW = 80;
  const barH = 6;

  ctx.fillStyle = 'rgba(255,255,255,0.15)';
  ctx.fillRect(barX, barY, barW, barH);

  // Filled portion
  const fill = ((tierIndex + 1) / tierCount) * barW;
  const colors = ['#4cff72','#a0ff40','#ffd700','#ff8c00','#ff4040','#ff0040'];
  ctx.fillStyle = colors[tierIndex] || '#ff0040';
  ctx.fillRect(barX, barY, fill, barH);

  // Label
  ctx.fillStyle = '#aaa';
  ctx.font = '10px "Segoe UI", sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('Speed ' + (tierIndex + 1), barX, barY - 4);
}

The bar fills up as the player advances through tiers, changing color from green to red. It is a subtle but effective way to communicate difficulty progression without cluttering the screen.

Step 5: Speed Cap

The minimum interval should never go below about 60ms. At 60ms per tick, the snake moves roughly 16 cells per second — fast enough to be challenging but still controllable. Going below 50ms makes the game feel unfair on most displays.

// Already handled in our tier system (last tier = 60ms)
// But if using the formula approach, always enforce:
function getSpeed() {
  const foodEaten = snake.length - 3;
  const speed = BASE_SPEED - (foodEaten * SPEED_STEP);
  return Math.max(speed, MIN_SPEED); // MIN_SPEED = 60
}
Tip: Test your speed cap on a 60Hz monitor. If the interval is shorter than one frame (~16.7ms), the game will skip visual frames and feel choppy. 60ms gives roughly 3-4 frames per tick, which looks smooth.

Step 6: Difficulty Curve

The difficulty curve is the relationship between progress and challenge. A good curve starts gentle and ramps up gradually. Here are three common approaches:

// Linear: constant increase per food
function linearSpeed(foodEaten) {
  return Math.max(BASE_SPEED - foodEaten * 5, MIN_SPEED);
}

// Logarithmic: fast early gains, then plateaus
function logSpeed(foodEaten) {
  const factor = Math.log2(foodEaten + 1) * 15;
  return Math.max(BASE_SPEED - factor, MIN_SPEED);
}

// Stepped: discrete jumps (our tier system)
function steppedSpeed(foodEaten) {
  return getSpeedTier().speed;
}

Linear is predictable — each food makes the same difference. Logarithmic feels fast at first but slows down, giving experienced players a longer challenge at high speed. Stepped creates clear milestones that feel like leveling up.

For most Snake games, stepped or linear works best. Logarithmic is better for games where you want a long endgame at near-max speed.

Tip: Playtest your curve. Numbers on paper mean nothing until you feel them. If the game feels too easy at 10 food, tighten the early tiers. If it feels impossible at 20 food, soften the later ones.

What's Next?

Speed increase gives the game a natural difficulty curve. In the next part, we add wall wrapping — instead of dying when hitting a wall, the snake appears on the opposite side. This creates a friendlier "kids mode" experience.

Continue to Part 4: Wall Wrapping →

More Articles

🔄
Tutorial Snake Wall Wrapping: Appear on the Other Side Tutorial · Apr 23, 2026 · 05:00 PM
📱
Tutorial Snake Mobile Controls: Swipe to Change Direction Tutorial · Apr 23, 2026 · 06:00 PM
🏆
Tutorial Snake High Score: Save and Display Best Scores Tutorial · Apr 23, 2026 · 03:00 PM
← Back to Blog