La cantidad de workers por defecto de Playwright es la mitad de las CPUs lógicas del equipo, así que un runner de CI con 2 núcleos obtiene 1 worker y ejecuta los tests secuencialmente por defecto. fullyParallel: true ejecuta cada test en paralelo sin importar el archivo, pero requiere que cada test sea dueño de sus datos por completo: dos tests que modifican la misma fila de base de datos con un ID hardcodeado van a competir entre sí y fallar de forma intermitente. Este artículo cubre la configuración de workers, los requisitos de aislamiento para fullyParallel, test.describe.serial() para grupos intencionalmente secuenciales, y el sharding con --shard para distribuir una suite grande entre máquinas en una matriz de GitHub Actions.
Cómo ejecuta Playwright los tests por defecto
Antes de cambiar cualquier configuración, conviene entender qué hace Playwright de fábrica.
Por defecto, Playwright ejecuta archivos de test en paralelo y tests dentro de un mismo archivo de forma secuencial. Cada proceso worker toma un archivo de test, ejecuta todos los tests en ese archivo de arriba a abajo, y luego toma el siguiente archivo disponible. Múltiples workers corren simultáneamente, cada uno manejando un archivo diferente.
La cantidad de workers por defecto es la mitad del número de CPUs lógicas del equipo. En una laptop de desarrollador típica con 8 núcleos obtienes 4 workers. En un runner de CI con 2 núcleos obtienes 1 worker, lo que significa que los tests se ejecutan secuencialmente a menos que lo sobreescribas.
// playwright.config.ts — comportamiento por defecto (no se requieren cambios)
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// workers tiene como valor por defecto la mitad de las CPUs lógicas
// los tests dentro de un archivo se ejecutan secuencialmente por defecto
});Este comportamiento por defecto es un buen punto de partida. Los tests en el mismo archivo suelen compartir estado de configuración de forma implícita (mismo page object, mismo flujo de login, mismos fixtures de datos), y la ejecución secuencial dentro del archivo los mantiene seguros. Los archivos separados se ejecutan simultáneamente, lo que da velocidad sin requerir aislamiento perfecto entre cada test individual.
Configurar workers en playwright.config.ts
La opción workers controla cuántos procesos paralelos ejecutan los tests. Se puede definir como número absoluto o como porcentaje de las CPUs disponibles.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Número absoluto — siempre usar exactamente 4 workers
workers: 4,
// O como porcentaje de las CPUs disponibles
// workers: '75%',
// O valores diferentes según el entorno
// workers: process.env.CI ? 2 : '50%',
});La forma de porcentaje es útil cuando querés que la misma configuración funcione en diferentes máquinas. '50%' en una máquina de 8 núcleos da 4 workers; en un runner de CI con 2 núcleos da 1. Le estás diciendo a Playwright "usa la mitad de la máquina" en lugar de hardcodear un número.
También puedes sobreescribir los workers desde la línea de comandos sin tocar la configuración:
# Ejecutar con un número específico de workers
npx playwright test --workers=4
# Forzar ejecución secuencial (1 worker)
npx playwright test --workers=1--workers=1 es útil para depurar problemas de aislamiento de tests. Si los tests pasan con 1 worker pero fallan con 4, hay un problema de estado compartido en algún lugar.
--workers=1. Si el test pasa de forma consistente, estás lidiando con una race condition o estado compartido entre tests, no con un bug en el test en sí.Modo fullyParallel: ejecutar todo a la vez
El modo estándar ejecuta archivos en paralelo pero tests dentro de un archivo de forma secuencial. fullyParallel: true elimina esa restricción. Cada test individual se ejecuta en paralelo sin importar en qué archivo esté.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // Todos los tests se ejecutan en paralelo
workers: 4,
});Esto puede reducir drásticamente el tiempo de ejecución en suites grandes. Una suite con 100 tests distribuidos en 10 archivos (si cada test tarda 2 segundos) baja de 20 segundos a unos 5 segundos con 4 workers en modo fullyParallel.
El tradeoff: fullyParallel requiere que cada test esté completamente aislado. Sin contexto de navegador compartido, sin estado de login compartido que se mute, sin tests que asumen que se ejecutan en un orden específico. Si tus tests escriben en un registro de base de datos compartido y ambos intentan modificar la misma fila simultáneamente, vas a obtener fallos intermitentes difíciles de reproducir.
Antes de habilitar fullyParallel, audita tu suite en busca de tests que crean datos con IDs hardcodeados (el usuario ID 123 es creado por el test A y eliminado por el test B), tests que dependen de que un test anterior ya se haya ejecutado, y estado a nivel de página que no se resetea entre tests.
Si tus tests usan test.beforeEach para hacer login fresco y trabajan con datos únicos, fullyParallel es seguro de habilitar. Si comparten un contexto de navegador pre-autenticado almacenado en una variable a nivel de módulo, no están listos para eso.
test.describe.serial() para tests intencionalmente secuenciales
A veces un grupo de tests genuinamente necesita ejecutarse en orden. Un flujo de checkout donde el test 1 agrega un ítem al carrito, el test 2 aplica un cupón, y el test 3 completa la compra: estos tests son inherentemente secuenciales. test.describe.serial() es la herramienta correcta para esto.
import { test, expect } from '@playwright/test';
test.describe.serial('flujo de checkout', () => {
test('agregar ítem al carrito', async ({ page }) => {
await page.goto('/products/widget-pro');
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
test('aplicar código de cupón', async ({ page }) => {
await page.goto('/cart');
await page.getByPlaceholder('Coupon code').fill('SAVE10');
await page.getByRole('button', { name: 'Apply' }).click();
await expect(page.getByTestId('discount-amount')).toBeVisible();
});
test('completar la compra', async ({ page }) => {
await page.goto('/checkout');
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByLabel('Expiry').fill('12/26');
await page.getByLabel('CVC').fill('123');
await page.getByRole('button', { name: 'Pay now' }).click();
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
});
});Con test.describe.serial(), Playwright ejecuta estos tres tests en orden y se detiene si alguno falla. No tiene sentido ejecutar "completar la compra" si "agregar ítem al carrito" falló.
Usa serial con moderación. Cada bloque serial es una sección de tu suite que no se puede paralelizar. Si te encuentras agregando serial a la mayoría de los bloques describe, la solución real es hacer tus tests independientes entre sí: generando datos de prueba únicos, usando contextos de navegador aislados, limpiando después de cada test.
Aislamiento de tests: el prerequisito para la ejecución paralela
La ejecución paralela amplifica los problemas de aislamiento. Un test que funciona bien solo fallará de forma impredecible cuando se ejecute al mismo tiempo que otro test que toca los mismos datos o estado.
El principio central: cada test debe ser dueño de sus datos y no depender de nada dejado por otro test.
import { test, expect } from '@playwright/test';
// MAL: estado compartido entre tests
let userId: number;
test('crea un usuario', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice', email: 'alice@example.com' }
});
userId = (await response.json()).id; // variable compartida — race condition esperando ocurrir
});
test('actualiza el usuario', async ({ request }) => {
// Si el test anterior no corrió todavía (o corrió en un worker diferente), userId es undefined
await request.put(`/api/users/${userId}`, {
data: { name: 'Alice Updated' }
});
});import { test, expect } from '@playwright/test';
// BIEN: cada test crea y es dueño de sus propios datos
test('actualiza un usuario', async ({ request }) => {
// Crear el usuario dentro de este test
const createResponse = await request.post('/api/users', {
data: {
name: 'Alice',
email: `alice-${Date.now()}@example.com` // email único para evitar conflictos
}
});
const { id } = await createResponse.json();
// Ahora actualizarlo — somos dueños de este usuario
const updateResponse = await request.put(`/api/users/${id}`, {
data: { name: 'Alice Updated' }
});
expect(updateResponse.status()).toBe(200);
});Para tests de UI, el fixture page de Playwright le da a cada test su propio contexto de navegador por defecto. Esa parte está resuelta. Los problemas de aislamiento generalmente vienen de los datos de test en una base de datos compartida, no del estado del navegador.
test.beforeAll para crear datos compartidos y test.afterAll para limpiarlos parece eficiente, pero crea dependencias ocultas entre tests. Si un test modifica los datos compartidos, los tests siguientes fallan. Prefiere test.beforeEach con datos por test, aunque sea más lento.Sharding: dividir la suite entre máquinas de CI
Los workers paralelizan tests dentro de una máquina. El sharding divide la suite de tests entre múltiples máquinas. Estos dos mecanismos son independientes y complementarios. Podés usar ambos juntos.
El flag --shard recibe un argumento actual/total:
# Ejecutar el shard 1 de 3 (primer tercio de los tests)
npx playwright test --shard=1/3
# Ejecutar el shard 2 de 3
npx playwright test --shard=2/3
# Ejecutar el shard 3 de 3
npx playwright test --shard=3/3Playwright distribuye los archivos de test de forma uniforme entre los shards. Con 30 archivos de test y 3 shards, cada shard recibe 10 archivos. La distribución es determinista. Vas a obtener los mismos archivos en el mismo shard en cada ejecución.
Podés combinar sharding con workers. Cada shard se ejecuta con múltiples workers, así obtienes paralelismo tanto dentro del shard como entre shards:
# Cada shard usa 4 workers internamente
npx playwright test --shard=1/3 --workers=4El sharding tiene valor principalmente en CI, donde podés aprovisionar múltiples máquinas para una sola ejecución del pipeline.
Matriz de GitHub Actions para sharding paralelo
GitHub Actions soporta builds en matriz, ejecutando un job varias veces con distintas entradas. Combinado con el sharding de Playwright, así es como distribuyes una suite lenta entre máquinas paralelas.
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run tests (shard ${{ matrix.shard }}/4)
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
BASE_URL: ${{ vars.BASE_URL }}
TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }}
- name: Upload shard report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 7fail-fast: false es importante aquí. Por defecto, si un job de la matriz falla, GitHub cancela los jobs restantes. Con fail-fast: false, todos los shards se ejecutan hasta completarse aunque uno falle. Obtienes el panorama completo de qué pasó y qué falló en toda la suite.
El argumento install chromium en el paso de instalación del navegador ahorra tiempo. Si estás ejecutando tests cross-browser, cambiá esto a --with-deps sin especificar un navegador para instalar los tres.
Si querés fusionar los reportes de los shards en un único reporte, agregá un job de merge después de que se completa la matriz:
merge-reports:
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Download all shard reports
uses: actions/download-artifact@v4
with:
pattern: playwright-report-shard-*
path: all-reports/
merge-multiple: false
- name: Merge reports
run: npx playwright merge-reports --reporter html ./all-reports/*/
- name: Upload merged report
uses: actions/upload-artifact@v4
with:
name: playwright-report-merged
path: playwright-report/
retention-days: 14Esto te da un único reporte HTML descargable que cubre todos los shards, con todos los resultados de los tests en un solo lugar.
Medir el speedup y encontrar la cantidad óptima de workers
Más workers no siempre significa tests más rápidos. Agregar workers incrementa la contención de recursos: más CPU, más memoria, más procesos de navegador compitiendo por la misma máquina. En algún punto, agregar otro worker ralentiza todo porque la máquina está sobrecargada.
La regla aproximada: workers = número de CPUs lógicas funciona bien para cargas de trabajo con uso intensivo de CPU. Para tests de navegador, que mayormente esperan respuestas de red y renderizado, a menudo podés ir más alto. Workers = 2x CPUs es un experimento razonable.
Cómo medirlo:
# Baseline: 1 worker (secuencial)
npx playwright test --workers=1 2>&1 | grep "passed\|failed\|Duration"
# Probar 2 workers
npx playwright test --workers=2 2>&1 | grep "passed\|failed\|Duration"
# Probar 4 workers
npx playwright test --workers=4 2>&1 | grep "passed\|failed\|Duration"
# Probar 8 workers
npx playwright test --workers=8 2>&1 | grep "passed\|failed\|Duration"Graficá los resultados. Buscas el punto de inflexión donde agregar workers deja de reducir el tiempo. Ese es tu número óptimo para esa máquina.
Para CI específicamente, verificá qué recursos provee tu runner. Los runners ubuntu-latest de GitHub Actions tienen 4 vCPUs y 16 GB de RAM. Con tests de navegador de Playwright, 4 workers es un buen punto de partida. Puede que obtengas entre 5 y 10% más velocidad con más workers, pero vas a empezar a ver presión de memoria con 8 o más workers en ese runner.
Una fórmula práctica para calcular el beneficio del sharding:
Tiempo con N shards ≈ (tiempo total de tests en 1 máquina) / N + overhead fijo por shard
Overhead fijo = checkout + npm ci + instalación del navegador ≈ 60 a 90 segundosSi tu suite tarda 10 minutos con 1 máquina, 4 shards la bajan a unos 2,5 minutos más 90 segundos de overhead, que son unos 4 minutos. Vale la pena. Si tu suite tarda 3 minutos, 4 shards la bajan a 45 segundos más 90 segundos, que son 2,5 minutos. No vale la complejidad adicional.
El umbral para empezar a considerar el sharding: cuando tu suite consistentemente tarda más de 5 minutos en una sola máquina de CI.
// playwright.config.ts — configuración paralela lista para producción
import { defineConfig } from '@playwright/test';
const isCI = !!process.env.CI;
export default defineConfig({
testDir: './tests',
// Paralelismo completo — requiere tests aislados
fullyParallel: true,
// Ajustar workers según el entorno
workers: isCI ? 4 : '50%',
// Reintentos solo en CI — no ocultar fallos en desarrollo local
retries: isCI ? 1 : 0,
// Timeout por test
timeout: 30_000,
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
},
});FAQ
Mis tests pasan en local pero fallan cuando se ejecutan en paralelo. ¿Por dónde empiezo?Corre con --workers=1 y confirma que los tests pasan. Luego prueba --workers=2. Si eso falla, tienes un problema de estado compartido entre exactamente dos tests que ahora se ejecutan simultáneamente. Revisa variables a nivel de módulo, filas de base de datos compartidas con IDs hardcodeados, o cualquier estado que persista entre tests. La solución casi siempre es mover el setup a beforeEach y usar identificadores únicos para los datos de prueba.
Playwright ordena los archivos de test alfabéticamente y los distribuye de forma round-robin entre los shards. No controlás la asignación directamente. Si un shard tarda consistentemente mucho más que los otros (porque tiene todos los tests lentos), considera dividir los archivos de test grandes en archivos más pequeños para que la distribución sea más pareja.
¿Puedo ejecutar tags específicos o patrones grep por shard en lugar de usar--shard?
Sí, y algunos equipos prefieren esto por predictibilidad: --grep @checkout en una máquina y --grep @catalog en otra. La desventaja es el mantenimiento manual: tienes que actualizar los patrones grep a medida que agregas tests. --shard es automático y no requiere mantenimiento.
fullyParallel: true afecta el orden en que aparecen los resultados en el reporte?
Sí. Con fullyParallel, los resultados aparecen a medida que los tests se completan, no en el orden de los archivos. El reporte HTML sigue agrupando por archivo y test, así que la legibilidad no se ve afectada. El output en la terminal simplemente se ve más mezclado.
workers en la configuración y --shard en la línea de comandos?
workers controla el paralelismo dentro de un proceso en una máquina. --shard divide la suite entre múltiples invocaciones, típicamente en diferentes máquinas. Operan en niveles diferentes y funcionan juntos. Cada shard puede tener múltiples workers.
→ See also: Depurando Tests Inestables: Una Guía Práctica | CI/CD para QA: GitHub Actions, Jenkins y GitLab Comparados | GitHub Actions para Tests de Playwright: La Configuración Completa (2026) | Aislamiento de Tests: Por qué Cada Test de Playwright Debe ser sin Estado | Archivo de Configuración de Playwright Explicado: Todas las Opciones