Skip to main content
The timeout middleware ensures that operations complete within a specified time limit. If an operation takes too long, it’s automatically aborted and a TimeoutError is thrown.

When to Use Timeout

External API calls

Prevent hanging on slow or unresponsive services

Database queries

Abort slow queries that might lock resources

User-facing operations

Ensure responsive UX by failing fast

Resource-intensive tasks

Limit CPU/memory usage by bounding execution time

Quick Start

import { r, globals } from "@bluelibs/runner";

const fetchData = r
  .task("api.fetchData")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 5000 }) // 5 second timeout
  ])
  .run(async (url: string) => {
    const response = await fetch(url);
    return response.json();
  })
  .build();
If the fetch takes longer than 5 seconds, a TimeoutError is thrown.

Configuration

ttl
number
required
Maximum time in milliseconds before the operation is aborted.Special values:
  • 0: Immediate timeout (useful for testing)
  • > 0: Normal timeout behavior

Examples

Basic Timeout

import { r, globals } from "@bluelibs/runner";

const slowQuery = r
  .task("db.slowQuery")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 10000 }) // 10 seconds
  ])
  .run(async (query: string) => {
    return database.execute(query);
  })
  .build();

Different Timeouts for Different Operations

// Fast timeout for user-facing operations
const getUserProfile = r
  .task("users.getProfile")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 2000 }) // 2 seconds
  ])
  .run(async (userId: string) => {
    return database.users.findOne({ id: userId });
  })
  .build();

// Longer timeout for background jobs
const generateReport = r
  .task("reports.generate")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 60000 }) // 1 minute
  ])
  .run(async (reportId: string) => {
    return reportGenerator.create(reportId);
  })
  .build();

Handling Timeout Errors

import { r, globals, run } from "@bluelibs/runner";
import { TimeoutError } from "@bluelibs/runner/globals/middleware/timeout.middleware";

const apiCall = r
  .task("api.call")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 5000 })
  ])
  .run(async (url: string) => {
    return fetch(url).then(r => r.json());
  })
  .build();

const app = r.resource("app").register([apiCall]).build();
const { runTask, dispose } = await run(app);

try {
  const result = await runTask(apiCall, "https://slow-api.example.com");
  console.log(result);
} catch (error) {
  if (error instanceof TimeoutError) {
    console.error("Operation timed out:", error.message);
    // "Operation timed out after 5000ms"
  } else {
    console.error("Other error:", error);
  }
}

await dispose();

Combining with Other Middleware

Timeout + Retry

import { r, globals } from "@bluelibs/runner";

const robustAPI = r
  .task("api.robust")
  .middleware([
    globals.middleware.task.retry.with({ retries: 3 }),
    globals.middleware.task.timeout.with({ ttl: 10000 }), // Each attempt gets 10s
  ])
  .run(async (url: string) => {
    return fetch(url).then(r => r.json());
  })
  .build();
Important: Each retry attempt gets its own timeout. If you have 3 retries and a 10-second timeout, the maximum total time is approximately 30 seconds (plus retry delays).The retry middleware is smart—it won’t retry if the operation was aborted by timeout.

Timeout + Circuit Breaker

const protectedAPI = r
  .task("api.protected")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 5000 }),
    globals.middleware.task.circuitBreaker.with({
      failureThreshold: 5,
      resetTimeout: 30000
    }),
  ])
  .run(async (url: string) => {
    return fetch(url).then(r => r.json());
  })
  .build();
Timeout errors count as failures for the circuit breaker. Multiple timeouts will trip the circuit.

Timeout + Cache

const cachedAPI = r
  .task("api.cached")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 5000 }),
    globals.middleware.task.cache.with({ ttl: 60000 }), // Cache for 1 minute
  ])
  .run(async (url: string) => {
    return fetch(url).then(r => r.json());
  })
  .build();
If the result is already cached, the timeout doesn’t apply—the cached value is returned immediately.

Execution Journal

The timeout middleware exposes an AbortController via the execution journal:
import { r, globals } from "@bluelibs/runner";
import { journalKeys } from "@bluelibs/runner/globals/middleware/timeout.middleware";

const fetchWithProgress = r
  .task("api.withProgress")
  .middleware([globals.middleware.task.timeout.with({ ttl: 10000 })])
  .run(async (url: string, deps, { journal }) => {
    // Access the AbortController created by timeout middleware
    const controller = journal.get(journalKeys.abortController);
    
    // Use it with fetch for proper cancellation
    const response = await fetch(url, { signal: controller?.signal });
    return response.json();
  })
  .build();

Journal Keys

journalKeys.abortController
AbortController
The AbortController created by the timeout middleware. Use its signal property to make operations cancellable.

Proper Cancellation with AbortSignal

For operations that support AbortSignal, pass the signal to enable immediate cancellation:
import { r, globals } from "@bluelibs/runner";
import { journalKeys } from "@bluelibs/runner/globals/middleware/timeout.middleware";

const cancellableFetch = r
  .task("api.cancellable")
  .middleware([globals.middleware.task.timeout.with({ ttl: 5000 })])
  .run(async (url: string, deps, { journal }) => {
    const signal = journal.get(journalKeys.abortController)?.signal;
    
    // Pass signal to fetch for immediate cancellation
    const response = await fetch(url, { signal });
    return response.json();
  })
  .build();
When you pass the AbortSignal to fetch (or other signal-aware APIs), the operation is cancelled immediately when the timeout triggers, freeing resources faster.

Common Patterns

Progressive Timeout

Different timeouts for different retry attempts:
import { r, globals } from "@bluelibs/runner";
import { journalKeys as retryKeys } from "@bluelibs/runner/globals/middleware/retry.middleware";

const progressiveTimeout = r
  .task("api.progressive")
  .middleware([
    globals.middleware.task.retry.with({ retries: 3 }),
  ])
  .run(async (url: string, deps, { journal }) => {
    const attempt = journal.get(retryKeys.attempt) ?? 0;
    const timeout = 5000 + (attempt * 2000); // 5s, 7s, 9s, 11s
    
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
      const response = await fetch(url, { signal: controller.signal });
      return response.json();
    } finally {
      clearTimeout(timeoutId);
    }
  })
  .build();

Conditional Timeout

const conditionalTimeout = r
  .task("api.conditional")
  .run(async (input: { url: string; priority: 'high' | 'low' }) => {
    const timeout = input.priority === 'high' ? 2000 : 10000;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
      const response = await fetch(input.url, { signal: controller.signal });
      return response.json();
    } finally {
      clearTimeout(timeoutId);
    }
  })
  .build();

Timeout with Fallback

const withFallback = r
  .task("api.withFallback")
  .middleware([
    globals.middleware.task.timeout.with({ ttl: 5000 }),
  ])
  .run(async (url: string) => {
    try {
      const response = await fetch(url);
      return response.json();
    } catch (error) {
      if (error instanceof TimeoutError) {
        // Return cached or default data on timeout
        return { cached: true, data: await getCachedData(url) };
      }
      throw error;
    }
  })
  .build();

Resource Timeout

You can also apply timeout to resource initialization:
import { r, globals } from "@bluelibs/runner";

const database = r
  .resource("app.db")
  .middleware([
    globals.middleware.resource.timeout.with({ ttl: 10000 }) // 10s to connect
  ])
  .init(async () => {
    const client = new MongoClient(process.env.DATABASE_URL);
    await client.connect();
    return client;
  })
  .dispose(async (client) => client.close())
  .build();
If resource initialization times out, the entire run() call will fail. Set generous timeouts for critical resources.

Best Practices

Choose timeouts based on actual operation performance:
// Too short - will timeout even on normal operations
.middleware([globals.middleware.task.timeout.with({ ttl: 100 })])

// Better - based on p95 latency + buffer
.middleware([globals.middleware.task.timeout.with({ ttl: 5000 })])
Always pass the abort signal to operations that support it:
const signal = journal.get(journalKeys.abortController)?.signal;
await fetch(url, { signal });
Timeout alone fails fast. Retry gives operations another chance:
.middleware([
  globals.middleware.task.retry.with({ retries: 2 }),
  globals.middleware.task.timeout.with({ ttl: 10000 }),
])
High timeout rates indicate systemic issues:
try {
  return await operation();
} catch (error) {
  if (error instanceof TimeoutError) {
    metrics.increment('task.timeout');
    logger.warn('Operation timed out', { task: 'api.call' });
  }
  throw error;
}
Use environment variables for configurable timeouts:
const timeout = parseInt(process.env.API_TIMEOUT || '5000', 10);

.middleware([
  globals.middleware.task.timeout.with({ ttl: timeout })
])

Timeout Error Details

The TimeoutError class extends RunnerError and includes:
import { TimeoutError } from "@bluelibs/runner/globals/middleware/timeout.middleware";

try {
  await runTask(myTask, input);
} catch (error) {
  if (error instanceof TimeoutError) {
    console.log(error.message);  // "Operation timed out after 5000ms"
    console.log(error.id);       // "runner.errors.middlewareTimeout"
    console.log(error.httpCode); // 408 (Request Timeout)
    console.log(error.data);     // { message: "Operation timed out after 5000ms" }
  }
}

See Also

Retry Middleware

Automatically retry failed operations

Circuit Breaker

Fail fast when services are unavailable

Custom Middleware

Build your own timeout strategies

Build docs developers (and LLMs) love