Ever waited for PR preview environments to spin up? Yeah, me too. Here's a pattern that changed the game for our team: pre-configured deployment slots with deterministic routing.

The Problem

Traditional PR preview workflows go something like this:

  1. Open PR
  2. CI/CD provisions a new environment
  3. Wait... ⏳
  4. Deploy code
  5. Wait some more... ⏳
  6. Finally get your preview URL

The provisioning step is the killer. Whether you're using Kubernetes namespaces, cloud functions, or edge workers, creating resources takes time.

The Solution: Pre-Configured Slots

What if we flipped the script? Instead of creating environments on-demand, we pre-configure a fixed set of deployment slots:

tokyo    🔗 https://tokyo.example.com
paris    🔗 https://paris.example.com
london   🔗 https://london.example.com
berlin   🔗 https://berlin.example.com
sydney   🔗 https://sydney.example.com
madrid   🔗 https://madrid.example.com
moscow   🔗 https://moscow.example.com
cairo    🔗 https://cairo.example.com
dubai    🔗 https://dubai.example.com
rome     🔗 https://rome.example.com

Then use a deterministic hash to map PR numbers to slots:

- uses: kriasoft/pr-codename@v1
  id: pr

- run: wrangler deploy --env ${{ steps.pr.outputs.codename }}

PR #1234 always maps to tokyo. PR #1235 always maps to paris. No provisioning, no waiting.

How It Works

The magic happens in three parts:

1. Pre-Configure Your Slots

First, set up your deployment slots. Here's a Cloudflare Workers example:

# wrangler.toml
[env.tokyo]
name = "preview-tokyo"
route = "tokyo.example.com/*"

[env.paris]
name = "preview-paris"
route = "paris.example.com/*"

[env.london]
name = "preview-london"
route = "london.example.com/*"

# ... repeat for all slots

2. Deterministic Mapping

The PR Codename Action uses a simple hash function to consistently map PR numbers to slot names:

const words = ["tokyo", "paris", "london", "berlin" /* ... */];
const index = prNumber % words.length;
return words[index];

The above is just an example, in reality it uses FNV-1a hashing algorithm.

3. Deploy to the Slot

Your GitHub Action workflow becomes dead simple:

name: Deploy PR Preview

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: kriasoft/pr-codename@v1
        id: pr

      - name: Deploy to slot
        run: |
          npm ci
          npm run build
          wrangler deploy --env ${{ steps.pr.outputs.codename }}

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 Preview deployed to https://${{ steps.pr.outputs.codename }}.example.com'
            })

The Benefits

This pattern isn't just a neat trick; it fundamentally changes the rhythm of your development cycle.

🚀 Zero-Wait Deploys The biggest win. By eliminating the on-demand provisioning step, deployments start immediately. What used to be a 2-3 minute coffee break is now a 30-second task. Your developers stay in the flow, and your pipeline gets a whole lot faster.

🔗 URLs You Can Actually Share Forget long, ugly, auto-generated URLs. With this pattern, PR #1234 always maps to https://tokyo.example.com. This URL is:

💰 No More Cloud Bill Surprises Dynamic environments are notorious for leaving behind orphaned resources that quietly drain your budget. With a fixed number of slots, your infrastructure costs become predictable. You know exactly what's running, and you never have to hunt down forgotten preview apps again.

🧹 Cleanup? What Cleanup? When a PR is merged or closed, there's no complex teardown script to run. The slot simply becomes available for the next PR. You can even have a workflow that automatically deploys the main branch to the slot to keep it fresh. It's a self-cleaning system.

Real-World Considerations

How Many Slots?

We've found 10-15 slots work well for most teams. The math:

Collision Handling

Yes, PRs can map to the same slot. PR #1 and PR #11 both map to the same environment with 10 slots. This means newer deployments overwrite older ones—so if you're reviewing PR #1 and someone pushes PR #11, your preview disappears.

In practice, this works for many teams because:

  1. Developers typically work on recent PRs
  2. Old PR previews naturally expire
  3. You can always trigger a redeploy to refresh

When slots don't work well: Large teams, high PR velocity, or when multiple people need to review the same PR simultaneously.

Database & Stateful Services

The biggest challenge with any preview environment is handling databases and stateful services. With slots, you have a few options:

For simple stateless apps, this isn't an issue. For complex apps with databases, it's the main implementation challenge.

Security Notes

Beyond Basic Previews

This pattern unlocks some cool possibilities:

Persistent Test Environments: QA can bookmark specific slots for testing.

A/B Testing: Map feature flags to slots for instant switching.

Geographic Testing: Actually deploy slots to different regions.

Try It Yourself

Getting started is pretty straightforward:

  1. Install the action:

    - uses: kriasoft/pr-codename@v1
      id: pr
    
  2. Use the codename in your deploy:

    deploy --env ${{ steps.pr.outputs.codename }}
    
  3. Enjoy instant PR previews 🚀

The full source is on GitHub if you want to customize the word list or hashing algorithm.

Slots vs On-Demand: Quick Comparison

Before you dive in, it's worth understanding how the pre-configured slots pattern stacks up against the traditional on-demand ephemeral environments. While this post focuses on slots, knowing the trade-offs helps you make the right choice for your team.

Factor

Pre-Configured Slots

On-Demand Ephemeral

Setup Speed

⭐⭐⭐⭐⭐ Instant (pre-warmed)

⭐⭐⭐ Takes minutes (provisioning)

Cost Predictability

⭐⭐⭐⭐⭐ Fixed monthly cost

⭐⭐ Variable usage-based

Scalability

⭐⭐ Hard limit on concurrent PRs

⭐⭐⭐⭐⭐ Scales with team size

Isolation

⭐⭐ PRs can overwrite each other

⭐⭐⭐⭐⭐ Each PR gets own environment

Production Fidelity

⭐⭐⭐ Prone to environment drift

⭐⭐⭐⭐⭐ Clean slate every time

Maintenance

⭐⭐⭐⭐ Simple for basic apps

⭐⭐ Complex (build) / ⭐⭐⭐⭐ (buy)

Developer Experience

⭐⭐ Can be confusing/frustrating

⭐⭐⭐⭐⭐ Smooth parallel workflows

Best for Slots: Small teams, simple apps, tight budgets

Best for On-Demand: Growing teams, complex apps, quality-focused

Nothing prevents you from mixing both approaches — use slots for rapid prototyping and on-demand for critical features.

Wrapping Up

Sometimes the best optimization is avoiding work altogether. By pre-configuring deployment slots and using deterministic routing, we eliminated the biggest bottleneck in our PR workflow.

Give it a shot and let me know how it works for your team. Happy deploying!


What patterns have you used for PR previews? Drop a comment below 👇 always curious to hear different approaches!