یک شب، سرور پروژه‌ای که دو سال بی‌دردسر کار کرده بود، شروع کرد به برگرداندن خطای 500 روی صفحه‌های گزارش. در لاگ، پیامی آشنا: Maximum execution time of 30 seconds exceeded. تا آن شب، همیشه این خطا را با یک عدد بزرگ‌تر در php.ini حل کرده بودم؛ ولی همان شب فهمیدم این خطا، خودش بیمار نیست، علامت بیمار است. خطای Maximum execution time در PHP یکی از آن خطاهایی است که اگر درست تشخیص داده نشود، هر بار دوباره برمی‌گردد - و هر بار با هزینه‌ی سنگین‌تری.

خطای Maximum execution time دقیقاً چه معنایی دارد؟

PHP زبانی است که برای اجرای اسکریپت‌های وب طراحی شده. PHP به‌عنوان زبان سمت سرور، در محیطی اجرا می‌شود که منابع سرور بین ده‌ها یا صدها پروسه‌ی همزمان تقسیم می‌شود. برای جلوگیری از اینکه یک اسکریپت بدنگار یا بی‌دقت، منابع سرور را برای همیشه قفل کند، PHP یک محدودیت زمانی سراسری دارد که به آن max_execution_time می‌گویند.

وقتی این محدودیت سر می‌رسد، PHP اسکریپت را متوقف می‌کند و پیام زیر را ثبت می‌کند:

Fatal error: Maximum execution time of 30 seconds exceeded in /path/to/file.php on line N

نکته‌ی مهم این است که این خطا، برخلاف Warning یا Notice، از نوع Fatal است. یعنی اسکریپت کاملاً متوقف می‌شود و هر کدی که بعد از آن نقطه بوده، اجرا نمی‌شود. اگر این خطا در میانه‌ی یک تراکنش دیتابیس رخ دهد، ممکن است داده‌ی نیمه‌کاره در سیستم بماند. همین ویژگی است که این خطا را جدی‌تر از سایر خطاها می‌کند.

یک نکته‌ی ظریف که بسیاری از توسعه‌دهنده‌های تازه‌کار نمی‌دانند: مقدار max_execution_time روی زمان CPU اسکریپت حساب می‌شود، نه زمان دیوار. یعنی فراخوانی‌های شبکه‌ای - مثل یک درخواست HTTP به سرور دیگر - روی این مقدار حساب نمی‌شوند. این تفاوت در موارد خاص می‌تواند باعث شود اسکریپتی که در ظاهر خیلی طول می‌کشد، خطا ندهد؛ و برعکس، اسکریپتی که به‌نظر سریع است، خطا بدهد. برای درک عمیق‌تر مبانی، ابتدا آموزش PHP از صفر برای مبتدیان را بخوانید.

خطای Maximum execution time یک شکست نیست، یک ترمز ایمنی است. اگر این ترمز نبود، یک اسکریپت معیوب می‌توانست کل سرور را به گروگان بگیرد.

چرا PHP محدودیت زمانی برای اجرای اسکریپت می‌گذارد؟

این سوال، ظاهراً ساده ولی عمیق است. وقتی توسعه‌دهنده با این خطا مواجه می‌شود، اولین واکنش طبیعی این است که بپرسد چرا PHP اجازه نمی‌دهد اسکریپت بیشتر اجرا شود. ولی نگاه حرفه‌ای این است که بپرسد چرا این محدودیت از ابتدا وجود دارد. جواب این سوال، کلید تصمیم‌گیری درست است.

سه دلیل بنیادین وجود دارد:

یک: محدودیت منابع سرور. یک سرور وب معمولی، همزمان به چندین یا چندصد درخواست پاسخ می‌دهد. اگر یک اسکریپت بتواند بی‌نهایت اجرا شود، آن یک اسکریپت می‌تواند تمام CPU و RAM سرور را برای خودش بگیرد و سایر درخواست‌ها را متوقف کند. این پدیده که به آن DoS طبیعی گفته می‌شود، در پروژه‌های بدون محدودیت رخ می‌دهد.

دو: بازخورد سریع به کاربر. کاربران وب، صبر محدودی دارند. اگر یک اسکریپت بیش از چند ثانیه طول بکشد، کاربر مرورگر را می‌بندد و به سایت دیگری می‌رود. محدودیت PHP، عملاً یک سیگنال است به توسعه‌دهنده که تجربه‌ی کاربری در خطر است.

سه: حفاظت از داده. اسکریپتی که نیمه‌کاره رها شود، می‌تواند داده‌ی ناسازگار تولید کند. محدودیت زمانی، در واقع یک حصار امنیتی است که جلوی نوشتن داده‌ی نیمه‌کاره را می‌گیرد. این موضوع در پروژه‌های مالی و تراکنشی اهمیت حیاتی دارد.

پس وقتی این خطا ظاهر می‌شود، پیام اصلی این است: اسکریپت شما دارد کار بزرگی می‌کند که در چارچوب استاندارد وب نمی‌گنجد. راه‌حل درست، نه حذف محدودیت است، نه بزرگ کردن کورکورانه‌ی آن، بلکه شکستن کار بزرگ به بخش‌های کوچک‌تر یا انتقال آن به بستر مناسب‌تر است. این فلسفه در مقاله‌ی بهینه‌سازی کدهای PHP با جزئیات بیشتر بررسی شده است.

کجا مقدار این محدودیت تنظیم می‌شود؟

مقدار max_execution_time در چند لایه قابل تنظیم است. تنظیم درست، بستگی به سناریو دارد. در تجربه‌ی من، درک این لایه‌ها و ترتیب اولویت آن‌ها، جلوی خیلی از سردرگمی‌ها را می‌گیرد.

لایه‌ی اول: فایل php.ini

این لایه، اصلی‌ترین و پایدارترین لایه است:

max_execution_time = 30

مقدار پیش‌فرض معمولاً 30 ثانیه است، ولی در برخی از هاست‌های اشتراکی می‌تواند 60 یا بیشتر باشد. تغییر این مقدار روی سرور، نیاز به دسترسی به فایل php.ini یا پنل هاست دارد. در هاست‌های اشتراکی، ممکن است این مقدار محدود باشد و نتوانید آن را افزایش دهید.

لایه‌ی دوم: فایل .htaccess

روی سرورهای Apache، می‌توانید این مقدار را در .htaccess تنظیم کنید:

php_value max_execution_time 60

این روش روی سرورهایی که PHP را به‌صورت module Apache اجرا می‌کنند کار می‌کند. روی سرورهای PHP-FPM کار نمی‌کند و خطای 500 برمی‌گرداند. این دام رایجی است که در پروژه‌های مهاجرت‌کرده دیده‌ام.

لایه‌ی سوم: تابع ini_set()

در ابتدای اسکریپت PHP:

ini_set("max_execution_time", "120");

این روش، انعطاف‌پذیرترین حالت است، ولی محدودیت دارد. در برخی از تنظیمات سرور که disable_functions دارد، این تابع کار نمی‌کند. همچنین روی برخی از سرویس‌های ابری که ini_set را محدود کرده‌اند، این راه‌حل جواب نمی‌دهد.

لایه‌ی چهارم: set_time_limit()

تابع مخصوص PHP برای این کار:

set_time_limit(120);

این تابع، تایمر را از لحظه‌ی فراخوانی ریست می‌کند. یعنی اگر اسکریپت 20 ثانیه اجرا شده باشد و بعد set_time_limit(120) فراخوانی شود، محدودیت جدید از لحظه‌ی فراخوانی حساب می‌شود. نکته‌ی مهم: این تابع برخلاف ini_set، در زمان‌های wait روی منابع خارجی، تایمر را متوقف نمی‌کند.

لایه‌ی پنجم: تنظیمات PHP-FPM

در سرورهای PHP-FPM که استاندارد روز شده‌اند، علاوه بر max_execution_time، پارامتری به نام request_terminate_timeout هم وجود دارد که از بیرون، اجرای اسکریپت را قطع می‌کند. اگر این مقدار کوچک‌تر از max_execution_time باشد، PHP-FPM قبل از PHP اسکریپت را قطع می‌کند و شما خطای Maximum execution time نمی‌بینید - به‌جای آن، خطای 504 یا صفحه‌ی سفید می‌بینید. این نکته در پروژه‌هایی که ناگهان روی FPM مهاجرت می‌کنند، بارها باعث سردرگمی شده است.

چرا افزایش ساده‌ی timeout راه‌حل نادرست است؟

شاید شما هم مثل من، در دوره‌ای این کار را کرده باشید: با دیدن خطای timeout، مقدار max_execution_time را از 30 به 300 یا 600 افزایش دادید. خطا مدتی پنهان شد، ولی چند هفته بعد با شکلی جدید برگشت - این بار به‌صورت قطع کاربران یا فرسودگی سرور.

دلیل این پدیده سه چیز است:

یک: تسک همچنان طول می‌کشد. افزایش timeout، علت را حل نمی‌کند. تسک همچنان 300 ثانیه طول می‌کشد، فقط دیگر خطا نمی‌دهد. اما در این 300 ثانیه، منابع سرور اشغال می‌شود و کاربر همچنان منتظر می‌ماند.

دو: مشکل از کد است، نه از سرور. اگر یک کوئری SQL بدون ایندکس، ساعت‌ها طول می‌کشد، ریشه در کوئری است، نه در محدودیت PHP. توضیح این نکته در مباحث خطاهای رایج MySQL دیده شده است. افزایش timeout فقط اثر را پنهان می‌کند.

سه: کاربر تجربه‌ی بدی دارد. چه تفاوتی برای کاربر دارد که بعد از 30 ثانیه صفحه‌ی خطا ببیند، یا بعد از 300 ثانیه صفحه‌ی موفق ببیند؟ در هر دو حالت، کاربر قبل از دریافت پاسخ سایت را ترک کرده است. تجربه‌ی کاربری، اصلاح timeout را بی‌فایده می‌کند.

پس رویکرد حرفه‌ای در برخورد با این خطا سه‌گانه است: اول تشخیص علت، دوم بازنویسی کد یا معماری، سوم تنظیم timeout به‌عنوان گزینه‌ی آخر. هر وقت دیدم که کسی اولین قدم را برداشته و آخرین قدم را هم برداشته، مطمئن شدم که دو قدم میانی را جا انداخته است. اگر با سایر خطاهای PHP هم مواجه هستید، الگوهای مشابه در خطای Warning در PHP هم به‌کار می‌آید.

افزایش timeout مثل بالا بردن سقف سرعت ماشینی است که موتورش مشکل دارد. مشکل را پنهان می‌کند ولی حل نمی‌کند، و در جایی دیگر بازمی‌گردد.

سناریوهای واقعی رخ دادن این خطا

در تجربه‌ی من روی پروژه‌های مختلف، این خطا در چند سناریوی مشخص مکرراً ظاهر می‌شود. شناخت این سناریوها، اولین گام در تشخیص سریع است.

سناریو اول: Import داده‌ی بزرگ

وقتی یک فایل CSV با صدها هزار ردیف را در دیتابیس وارد می‌کنید، PHP باید هر ردیف را پردازش کند. اگر پردازش هر ردیف 1 میلی‌ثانیه طول بکشد، 100,000 ردیف یعنی 100 ثانیه. این از 30 ثانیه بیشتر است و خطا ظاهر می‌شود.

راه‌حل درست: پردازش دسته‌ای (Batch Processing). هر بار 500 ردیف پردازش شود، سپس با کمک AJAX یا Queue، درخواست بعدی ارسال شود. این رویکرد، تک تک درخواست‌ها را زیر 30 ثانیه نگه می‌دارد.

سناریو دوم: کوئری سنگین بدون ایندکس

یک کوئری SELECT روی جدولی با میلیون‌ها ردیف، اگر روی ستون‌های بدون ایندکس اجرا شود، می‌تواند دقیقه‌ها طول بکشد. این مورد در پروژه‌های فروشگاهی با جداول سفارش و تراکنش زیاد، بسیار شایع است. رفع اصولی در مباحث بهینه‌سازی کوئری‌های MySQL به‌تفصیل آمده است.

سناریو سوم: تماس با API خارجی

اگر اسکریپت شما به یک API خارجی متصل می‌شود و آن API کند پاسخ می‌دهد، زمان اجرا بالا می‌رود. نکته‌ی ظریف: زمان wait روی شبکه، در max_execution_time حساب نمی‌شود، ولی روی زمان واقعی کاربر بله. اگر API در سطح خودش کند باشد، اغلب خطای timeout از آن سمت می‌آید و شما فقط یک connection timeout می‌بینید.

سناریو چهارم: پردازش تصویر یا فایل بزرگ

کتابخانه‌هایی مثل GD یا Imagick برای پردازش تصویر، هر تصویر بزرگ را در چند مرحله بازمی‌کنند. پردازش 100 تصویر بزرگ، به‌راحتی از 30 ثانیه رد می‌شود. راه‌حل: استفاده از صف (Queue) و پردازش خارج از request کاربر.

سناریو پنجم: حلقه‌ی بازگشتی نامتناهی

یک باگ منطقی در کد می‌تواند باعث شود یک حلقه‌ی بازگشتی هرگز تمام نشود. این حالت، بدترین سناریو است، چون نه‌فقط timeout می‌دهد، بلکه منابع سرور را تا لحظه‌ی قطع، اشغال می‌کند. تشخیص: در stack trace، تابع تکراری دیده می‌شود.

سناریو ششم: Cron Job سنگین

Cron Jobهایی که برای sync کردن داده یا پاک‌سازی جدول طراحی شده‌اند، اگر بدون محدودیت دسته‌بندی طراحی شوند، به‌راحتی از 30 ثانیه رد می‌شوند. این سناریو در پروژه‌های وردپرسی که wp-cron روی ترافیک واقعی اجرا می‌شود، بسیار شایع است.

سناریو هفتم: اجرای Migration سنگین در Framework

در فریم‌ورک‌هایی مثل Laravel یا Django، migrationهایی که به‌طور مثال ستون جدیدی با مقدار پیش‌فرض اضافه می‌کنند به تمام ردیف‌ها، می‌توانند زمان زیادی ببرند. راه‌حل: اجرای migration از CLI با timeout تنظیم‌شده، نه از طریق وب.

روش تشخیص اصولی خطا در سه گام

وقتی این خطا ظاهر می‌شود، اولین کار شناسایی محل دقیق و علت ریشه‌ای است. این سه‌گام، چیزی است که در تمام پروژه‌ها اجرا می‌کنم.

گام اول: یافتن خط دقیق

پیام خطا، شماره فایل و شماره خط را می‌دهد. این نقطه، تقریباً همیشه یک حلقه یا یک تابع سنگین است. اگر شماره خط دقیق نیست - که در بعضی موارد رخ می‌دهد - از ابزارهایی مثل Xdebug استفاده کنید تا stack trace کامل را ببینید.

// نمونه‌ای از لاگ کردن زمان اجرا در نقاط حساس
$start = microtime(true);
// ... کد سنگین
error_log("Execution time: " . (microtime(true) - $start));

این تکنیک ساده، اغلب سریع‌تر از هر ابزار پیچیده‌ای جواب می‌دهد. با اضافه کردن این خطوط در نقاط مختلف، می‌توانید بخش کند را دقیق پیدا کنید.

گام دوم: تحلیل queries و I/O

اگر بخش کند، یک حلقه با کوئری‌های دیتابیس است، با EXPLAIN در MySQL بررسی کنید که آیا از ایندکس استفاده می‌کند یا نه. در تجربه‌ی من، بیش از نیمی از موارد timeout، ریشه در کوئری‌های بدون ایندکس دارند. مبانی کار با دیتابیس در یادگیری MySQL از صفر باز شده است.

گام سوم: استفاده از Profiler

ابزارهایی مثل Xdebug Profiler یا Blackfire، می‌توانند دقیقاً نشان دهند که کدام تابع چند درصد از زمان اجرا را گرفته است. این ابزارها در پروژه‌های پیچیده، تفاوت جدی ایجاد می‌کنند. راه‌اندازی Xdebug Profiler چند دقیقه وقت می‌گیرد، ولی در عوض، یک نقشه‌ی دقیق از عملکرد کد شما می‌دهد.

در کنار این سه گام، یک تکنیک حرفه‌ای هم هست: profiling در محیط staging با همان بار داده‌ی production. اگر در محیط development با 100 ردیف تست کنید، هر اسکریپتی سریع است. ولی در production با 10 میلیون ردیف، همان اسکریپت ممکن است نیم ساعت طول بکشد. تست در محیط staging با داده‌ی production، واقعی‌ترین تصویر را می‌دهد.

بازنویسی کد به‌عنوان راه‌حل پایدار

بعد از تشخیص، نوبت به بازنویسی کد می‌رسد. بازنویسی، در بیشتر موارد، روی سه محور انجام می‌شود: کاهش تعداد عملیات، کاهش پیچیدگی هر عملیات، و توزیع عملیات در زمان.

کاهش تعداد عملیات

مثال کلاسیک: حلقه‌ای که در هر تکرار یک کوئری به دیتابیس می‌زند. الگوی N+1، معروف‌ترین دام در این زمینه است:

// بدترین الگو - یک کوئری برای هر آیتم
$users = get_users();
foreach ($users as $user) {
    $orders = get_orders_for_user($user->id);
}

// الگوی بهینه - یک کوئری برای همه
$users = get_users();
$user_ids = array_column($users, "id");
$orders = get_orders_for_users($user_ids);

این تغییر ساده، می‌تواند زمان اجرا را از دقیقه به ثانیه برساند. الگوهای مشابه در کار با آرایه‌های PHP به‌تفصیل آمده است.

کاهش پیچیدگی هر عملیات

بعضی مواقع، هر عملیات به‌تنهایی کند نیست، ولی تعدادشان زیاد است. در این حالت، باید هر عملیات را ساده‌تر کرد. مثال: جایگزینی loopهای تودرتوی پیچیده با ساختارهای داده‌ی مناسب. یا جایگزینی محاسبه‌ی مجدد با memoization.

توزیع عملیات در زمان

اگر عملیات را نمی‌توان کم کرد و نمی‌توان ساده کرد، بهترین راه، توزیع آن در چند درخواست است. این الگو، همان چیزی است که در ادامه به‌عنوان Background Job و Batch Processing بررسی می‌شود.

یک نکته‌ی ظریف: بازنویسی کد، فرصت خوبی است برای بهبود معماری کلی. اگر کد شما در یک نقطه timeout می‌دهد، احتمالاً در نقاط دیگر هم مشکل مشابه وجود دارد که هنوز بروز نکرده. بازنویسی سیستماتیک، از بروز مشکل در نقاط پنهان جلوگیری می‌کند.

وقتی یک نقطه از کد شما timeout می‌دهد، مطمئن باشید که ده نقطه دیگر هم در آستانه‌ی همین خطا هستند. مشکل، نقطه‌ای نیست؛ الگویی است.

کارهای طولانی و الگوی Background Job

برای کارهایی که واقعاً نمی‌توانند سریع انجام شوند - مثل تولید گزارش سالانه، پردازش تصاویر دسته‌ای، یا ارسال ایمیل انبوه - الگوی درست، انتقال کار به پشت صحنه است. این الگو، در پروژه‌های حرفه‌ای، استاندارد است.

چرا کاربر نباید منتظر بماند؟

وقتی کاربر روی دکمه‌ی تولید گزارش کلیک می‌کند، فرض درست این است که گزارش در پشت صحنه آماده می‌شود و کاربر در زمانی دیگر - مثلاً از طریق ایمیل - از آمادگی آن مطلع می‌شود. این رویکرد، تجربه‌ی کاربری را چند برابر بهبود می‌دهد و مشکل timeout را کاملاً حل می‌کند.

ابزارهای Queue در PHP

برای پیاده‌سازی Queue، ابزارهای مختلفی وجود دارد:

  • Redis + PHP-Resque: برای پروژه‌های متوسط، سریع و ساده.
  • RabbitMQ + php-amqplib: برای پروژه‌های بزرگتر با نیاز به پیام‌رسانی پیشرفته.
  • Laravel Queue: اگر با Laravel کار می‌کنید، یک سیستم Queue کامل و آماده.
  • Symfony Messenger: برای پروژه‌های Symfony.
  • AWS SQS: اگر روی AWS هستید.

در پروژه‌های وردپرسی، افزونه‌های Action Scheduler به‌عنوان یک راه‌حل داخلی، بسیار مفید است. ولی اگر حجم کار بسیار بالاست، استفاده از یک Queue مستقل با workerهای مجزا، تصمیم حرفه‌ای‌تری است.

الگوی پیاده‌سازی

ساختار استانداردی که در پروژه‌ها استفاده می‌کنم:

// مرحله ۱: کاربر درخواست را ثبت می‌کند
$job_id = queue_push("generate_report", [
    "user_id" => $user_id,
    "date_from" => $date_from,
    "date_to" => $date_to
]);

// مرحله ۲: به کاربر پیام فوری داده می‌شود
return response()->json([
    "message" => "گزارش در حال آماده‌سازی است",
    "job_id" => $job_id
]);

// مرحله ۳: worker جداگانه job را پردازش می‌کند
// (در فایل worker.php و با اجرای دائمی)
while ($job = queue_pop("generate_report")) {
    generate_report($job["data"]);
    queue_mark_done($job["id"]);
}

این الگو، در سرورهای معمولی PHP، با کمی تنظیم، به‌سادگی قابل پیاده‌سازی است. اجرای worker می‌تواند از طریق Supervisor یا systemd مدیریت شود.

پردازش داده‌ی بزرگ با Streaming و Batch

وقتی با حجم زیادی از داده کار می‌کنید - مثلاً خواندن یک فایل 500 مگابایتی، یا پردازش یک جدول با 10 میلیون ردیف - رویکرد خواندن همه در حافظه و بعد پردازش، منجر به timeout و memory limit می‌شود. راه‌حل، استفاده از Streaming است.

Streaming در خواندن فایل

برای فایل‌های بزرگ CSV، از fgetcsv() در یک حلقه استفاده کنید، نه file_get_contents():

$handle = fopen("large.csv", "r");
while (($row = fgetcsv($handle)) !== false) {
    // پردازش هر ردیف
    // هر ردیف در حافظه باقی نمی‌ماند
}
fclose($handle);

این تکنیک، حافظه را ثابت نگه می‌دارد و می‌تواند میلیون‌ها ردیف را پردازش کند. جزئیات کامل در خطای Memory limit در PHP آمده است.

Streaming در دیتابیس

برای دیتابیس، به‌جای SELECT * FROM large_table که همه‌ی ردیف‌ها را در حافظه می‌گیرد، از cursor یا unbuffered query استفاده کنید. در PDO:

$pdo = new PDO($dsn, $user, $pass, [
    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false
]);
$stmt = $pdo->query("SELECT * FROM large_table");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // پردازش هر ردیف
}

الگوهای کامل اتصال و کوئری در آموزش PDO در PHP و مبانی اتصال PHP به MySQL به‌تفصیل باز شده است.

Batch Processing

برای عملیات نوشتن - مثل به‌روزرسانی انبوه - عملیات را به دسته‌های کوچک تقسیم کنید:

$batch_size = 500;
$offset = 0;
while (true) {
    $rows = fetch_batch($offset, $batch_size);
    if (empty($rows)) {
        break;
    }
    process_batch($rows);
    $offset += $batch_size;
    usleep(100000); // 0.1 ثانیه استراحت
}

نکته‌ی ظریف: در این الگو، اگر پردازش یک دسته از timeout نزدیک شود، می‌توانید بعد از هر دسته بررسی کنید و در صورت لزوم ادامه را به فراخوانی بعدی منتقل کنید. این تکنیک در پروژه‌های بزرگ، استاندارد است.

نقش Cache در کاهش زمان اجرا

بسیاری از مواقع، عملیات کند به این دلیل طول می‌کشد که داده از صفر محاسبه می‌شود، در حالی که می‌توانست از قبل ذخیره شده باشد. استفاده‌ی درست از Cache، می‌تواند زمان اجرا را چند برابر کاهش دهد.

Cache در سه لایه

سه لایه‌ی Cache در PHP وجود دارد که در پروژه‌ها استفاده می‌کنم:

لایه‌ی اول - OpCache: در سطح PHP، کد کامپایل‌شده را Cache می‌کند. با فعال‌سازی OpCache، زمان parse فایل‌ها به‌شدت کاهش می‌یابد. این لایه تقریباً همیشه باید فعال باشد و در تنظیمات production، استاندارد است.

لایه‌ی دوم - APCu یا Redis: برای Cache کردن داده‌های محاسبه‌شده در سطح اپلیکیشن. مثال: خروجی یک API خارجی که هر ساعت یک‌بار عوض می‌شود، می‌تواند در APCu ذخیره شود و درخواست‌های بعدی سریع‌تر پاسخ بگیرند.

لایه‌ی سوم - HTTP Cache: در سطح کاربر یا CDN. اگر محتوایی قابل Cache کردن است، با تنظیم هدرهای مناسب، می‌توانید درخواست را قبل از رسیدن به PHP، پاسخ دهید.

چرا Cache در محیط توسعه فعال نیست؟

در محیط development، معمولاً Cache غیرفعال است تا تغییرات فوری اعمال شوند. این کار باعث می‌شود بعضی از خطاهای مربوط به timeout در development اصلاً دیده نشوند، ولی در production که Cache فعال است، پنهان شوند. توصیه: در staging، Cache را مانند production فعال کنید تا سناریوهای واقعی را ببینید.

در پروژه‌های وردپرسی، Cache را با افزونه‌های مخصوص مدیریت می‌کنند. ولی حتی در پروژه‌های غیر وردپرسی، وجود یک لایه‌ی Cache استاندارد از ابتدای پروژه، می‌تواند جلوی بسیاری از خطاهای performance را بگیرد.

این خطا در محیط production چه تفاوتی دارد؟

در محیط development، خطای timeout نسبتاً بی‌خطر است - لاگ را می‌بینید و کد را اصلاح می‌کنید. در production، این خطا ابعاد دیگری پیدا می‌کند.

پیامدهای پنهان در production

وقتی این خطا در production رخ می‌دهد، پیامدهایی دارد که در development دیده نمی‌شوند:

  1. تراکنش نیمه‌کاره: اگر اسکریپت در میانه‌ی یک تراکنش متوقف شود، داده‌ی ناقص در دیتابیس باقی می‌ماند. این سناریو در پروژه‌های مالی، می‌تواند به اختلاف عددی جدی منجر شود.
  2. صف انباشته: اگر چندین درخواست همزمان روی یک اسکریپت سنگین بیایند، همه timeout می‌دهند ولی منابع سرور را مصرف کرده‌اند. صف درخواست‌ها انباشته می‌شود و سایت به‌کندی پاسخ می‌دهد.
  3. تأثیر بر سایر سرویس‌ها: اگر اسکریپت شما با یک سرویس خارجی کار می‌کند و در نیمه‌راه timeout می‌دهد، سرویس خارجی ممکن است به‌اشتباه فکر کند که شما ناتمام رها کرده‌اید و داده‌ی ناقص ثبت کند.
  4. افزایش هزینه‌ی سرور: در سرویس‌های ابری با پرداخت بر اساس مصرف، هر اسکریپت timeout، هزینه‌ی محاسبه‌ای را مصرف می‌کند بدون اینکه بازدهی داشته باشد.

راهبرد مدیریت در production

روی محیط production، رویکرد متفاوتی اجرا می‌کنم:

یک: لاگ‌گیری ساختارمند از هر timeout. اگر اسکریپتی timeout می‌دهد، باید در داشبورد monitoring ظاهر شود. بدون این، دفعات بعدی خطا کشف نمی‌شود.

دو: Alerting روی نرخ timeout. اگر نرخ در بازه‌ی کوتاه بالا رفت، یعنی مشکل سیستمی است و نیاز به بررسی فوری دارد.

سه: کد اصلاح، نه افزایش timeout. در production، هر بار که timeout رخ می‌دهد، بررسی ریشه‌ای باید در دستور کار قرار بگیرد.

لایه‌ی امنیتی مرتبط با این موضوع در مباحث امنیت در PHP هم بررسی شده است - چون بعضی از timeoutها می‌توانند نشانه‌ی تلاش‌های DoS یا سوءاستفاده باشند.

اشتباهات رایج در برخورد با این خطا

در سال‌ها کار با این خطا، الگوهای تکراری از اشتباهات دیده‌ام. این پنج اشتباه، بیشترین فراوانی را دارند و هر کدام می‌تواند پروژه را به چالش بکشد:

اشتباه اول: افزایش کورکورانه‌ی timeout

این شایع‌ترین اشتباه است. افزایش timeout، علت را پنهان می‌کند ولی حل نمی‌کند. در کوتاه‌مدت، خطا برطرف می‌شود؛ در بلندمدت، تجربه‌ی کاربر بدتر می‌شود و سرور فشار بیشتری می‌کشد. راه‌حل: افزایش timeout را فقط بعد از تشخیص و مستندسازی ریشه اجرا کنید.

اشتباه دوم: نادیده گرفتن پیام خطا

پیام خطا، شماره خط دقیق را می‌دهد. بسیاری از توسعه‌دهنده‌ها سراغ گوگل می‌روند بدون اینکه خط را در کد خودشان ببینند. نگاه کردن به خط و سه خط قبل و بعد، تقریباً همیشه سرنخ اولیه را می‌دهد. راه‌حل: قبل از جستجو، کد را ببینید.

اشتباه سوم: استفاده از @set_time_limit در حلقه

بعضی از توسعه‌دهنده‌ها فکر می‌کنند اگر set_time_limit(0) را در ابتدای اسکریپت بگذارند، مشکل حل می‌شود. این کار عملاً محدودیت را حذف می‌کند و اسکریپت می‌تواند برای همیشه اجرا شود. نتیجه: قفل شدن پروسه و کاهش منابع سرور. راه‌حل: هرگز محدودیت را کامل حذف نکنید.

اشتباه چهارم: عدم استفاده از Queue برای تسک‌های سنگین

بعضی از تیم‌ها، به‌جای ایجاد یک سیستم Queue، سعی می‌کنند timeout را بزرگ کنند و کاربر منتظر بماند. این رویکرد در کوتاه‌مدت جواب می‌دهد، ولی در بلندمدت تجربه‌ی کاربر را تخریب می‌کند. راه‌حل: هر کاری که بیش از 5 ثانیه طول می‌کشد، باید به Queue منتقل شود.

اشتباه پنجم: تست نکردن با داده‌ی واقعی

اگر با 100 ردیف تست کنید، هر اسکریپتی سریع است. مشکل وقتی ظاهر می‌شود که در production با 10 میلیون ردیف مواجه شوید. راه‌حل: در staging، داده‌ی مشابه production را داشته باشید.

اشتباه ششم: عدم مانیتورینگ طولانی‌مدت

بدون داشبورد monitoring، نمی‌توانید بفهمید که مثلاً نرخ timeout در سه ماه گذشته چقدر تغییر کرده. اگر نرخ به‌آرامی افزایش یافته، یعنی کد شما به‌آرامی سنگین‌تر شده. راه‌حل: لاگ و پایش مداوم، بخشی از فرآیند تولید است.

اشتباه هفتم: کپی-پیست راه‌حل‌های اینترنتی

بعضی از راه‌حل‌های اینترنتی مثل تغییر مستقیم در فایل‌های هسته‌ی فریم‌ورک، در کوتاه‌مدت کار می‌کنند، ولی در آپدیت‌های بعدی از دست می‌روند. راه‌حل: همیشه راه‌حل را در لایه‌ی کاربردی (application layer) اعمال کنید، نه در هسته.

در برخورد با خطای timeout، پاسخ درست تقریباً همیشه ساده‌تر از پاسخ سریع است. ساده‌ترین راه‌حل، تقریباً همیشه اشتباه‌ترین راه‌حل هم هست.

پرسش‌های پرتکرار درباره خطای Maximum execution time

این پرسش‌ها از دل تجربه‌ی عملی و جلسات مشاوره با تیم‌های فنی جمع‌آوری شده‌اند. پاسخ هر کدام بر اساس سناریوهای واقعی است.

چرا خطای Maximum execution time روی سایت‌های اشتراکی بیشتر دیده می‌شود؟

چون در هاست‌های اشتراکی، منابع سرور بین ده‌ها یا صدها سایت مشترک تقسیم می‌شود. برای اینکه یک سایت نتواند منابع را برای خودش بگیرد، مقدار max_execution_time کوچک نگه داشته می‌شود. اگر سایت شما تسک سنگینی دارد، در هاست اشتراکی با محدودیت مواجه می‌شود. راه‌حل: یا معماری سایت را با Queue سازگار کنید، یا به سرور مجازی مهاجرت کنید.

آیا می‌توانم max_execution_time را روی بی‌نهایت بگذارم؟

فنی بله، ولی توصیه نمی‌شود. مقدار 0 به PHP می‌گوید محدودیتی اعمال نکن. این کار در اسکریپت‌های CLI مثل cron job کاربرد دارد، ولی در اسکریپت‌های وب، ریسک جدی دارد. اگر کد شما باگ منطقی داشته باشد، سرور را برای همیشه قفل می‌کند. راه‌حل امن: مقدار بزرگ ولی محدود - مثل 300 ثانیه - و استفاده از Queue برای تسک‌های سنگین.

چرا خطای timeout در CLI دیده نمی‌شود؟

چون در حالت CLI، PHP به‌طور پیش‌فرض محدودیت زمانی ندارد. مقدار max_execution_time در CLI برابر 0 است. این طراحی، برای cron jobها و اسکریپت‌های طولانی منطقی است. اگر می‌خواهید در CLI هم محدودیت داشته باشید، باید دستی با set_time_limit() تنظیم کنید.

تفاوت max_execution_time و max_input_time چیست؟

max_execution_time محدودیت زمان پردازش اسکریپت است. max_input_time محدودیت زمان parse و آماده‌سازی ورودی‌هاست (مثل آپلود فایل‌های بزرگ). این دو مستقل هستند و می‌توانند در سناریوهای مختلف خطای جداگانه بدهند. اگر آپلود فایل بزرگ دارید، ممکن است به max_input_time برسید، نه به max_execution_time.

آیا این خطا در PHP 8 تفاوتی با PHP 7 دارد؟

خود پیام خطا تفاوت محسوسی ندارد، ولی رفتار PHP 8 در برخی شرایط سخت‌گیرانه‌تر شده است. تغییرات اصلی در نوع خطاهای دیگر (مثل Warning) دیده می‌شود. تفاوت‌های نسخه‌ای در تفاوت PHP 7 و PHP 8 به‌تفصیل آمده است.

چگونه بفهمم کدام بخش کد من timeout می‌دهد؟

سه روش موثر: اول، اضافه کردن microtime(true) در نقاط مختلف کد و لاگ کردن تفاوت‌ها. دوم، استفاده از Xdebug Profiler برای گرفتن پروفایل کامل. سوم، افزودن لاگ SQL در سطح دیتابیس برای دیدن کوئری‌های کند. ترکیب سه روش، تصویر کاملی می‌دهد.

آیا error_reporting روی نمایش این خطا تأثیر دارد؟

خیر. خطای Maximum execution time از نوع Fatal است و همیشه نمایش داده می‌شود، حتی اگر display_errors خاموش باشد. در محیط production، این خطا در لاگ سرور ثبت می‌شود حتی اگر در مرورگر دیده نشود. برای دیباگ، لاگ سرور را بررسی کنید، نه فقط مرورگر.

آیا این خطا می‌تواند ناشی از حمله باشد؟

بله، در بعضی موارد. مهاجم می‌تواند درخواست‌هایی طراحی کند که منابع سرور را مصرف کنند. مثلاً درخواست‌هایی با پارامترهای بزرگ که کوئری‌های سنگین تولید می‌کنند. اگر خطای timeout به‌طور غیرعادی بالا رفت، به احتمال حمله‌ی DoS شک کنید. امنیت PHP در چنین سناریوهایی مباحث اختصاصی دارد.

آیا Queue واقعاً راه‌حل کامل این خطاست؟

Queue راه‌حل برای تسک‌های واقعاً طولانی است، نه همه موارد. اگر تسک شما 3 ثانیه طول می‌کشد و در شرایط خاص به 35 ثانیه می‌رسد، راه‌حل بهینه‌سازی کوئری است، نه انتقال به Queue. تشخیص بین این دو حالت، بر اساس تحلیل داده‌های واقعی است.

چگونه در وردپرس timeout را مدیریت کنم؟

در وردپرس، ابتدا با افزونه‌های debug مثل Query Monitor ببینید کدام کوئری یا hook زمان زیادی می‌گیرد. اگر از افزونه‌ی مشکل‌دار است، به‌روزرسانی یا جایگزینی آن را در دستور کار بگذارید. اگر کد خودتان است، با الگوهای Batch و Cache بهینه کنید. برای wp-cron، از cron سرور استفاده کنید، نه cron داخلی وردپرس.

آیا با افزایش RAM مشکل حل می‌شود؟

خیر. Memory و Timeout دو مقوله‌ی متفاوت هستند. RAM برای ذخیره‌ی داده در حافظه کاربرد دارد؛ timeout برای محدودیت زمانی. اگر کد شما 100 مگابایت RAM مصرف می‌کند ولی از 30 ثانیه نمی‌گذرد، افزایش RAM کمکی نمی‌کند. دو مقوله‌ی جداگانه‌اند که در خطای Memory limit در PHP جداگانه بررسی شده‌اند.

اگر set_time_limit کار نکرد چه کنم؟

در بعضی از هاست‌ها، set_time_limit در disable_functions قرار دارد. در این حالت، باید با پشتیبانی هاست صحبت کنید. راه‌حل جانشین: معماری کد را طوری بازنویسی کنید که هر درخواست کوتاه باشد و از Queue برای تسک‌های سنگین استفاده کنید.

تفاوت زمان دیوار و زمان CPU در محاسبه timeout چطور است؟

max_execution_time روی زمان CPU محاسبه می‌شود، نه زمان دیوار. عملیات I/O مثل خواندن فایل، کوئری دیتابیس، یا فراخوانی API، در این محاسبه اغلب لحاظ نمی‌شوند (به‌خصوص در سیستم‌های Unix). ولی روی بعضی از سیستم‌ها مثل Windows، این تفاوت متفاوت عمل می‌کند. در production، باید در نظر بگیرید که کاربر زمان دیوار را تجربه می‌کند، نه CPU را.

آیا می‌توانم timeout را فقط برای یک درخواست خاص تنظیم کنم؟

بله، با set_time_limit() در ابتدای اسکریپت. این رویکرد در سناریوهایی که فقط یک اسکریپت خاص نیاز به زمان بیشتری دارد، مناسب است. مثال کلاسیک: اسکریپت export داده که از طریق admin trigger می‌شود و کاربر می‌داند که ممکن است چند دقیقه طول بکشد.

چرا بعد از افزودن Cache، همچنان timeout می‌گیرم؟

احتمالاً Cache به‌درستی اعمال نشده یا در مسیر بحرانی قرار نگرفته. در Cache، کلید طراحی مهم است: اگر کلید شامل پارامترهای متغیر باشد، هر درخواست یک کلید جدید تولید می‌کند و Cache عملاً بی‌اثر می‌شود. با لاگ کردن cache hit/miss، می‌توانید این را تشخیص دهید.

آیا این خطا با خطای 504 Gateway Timeout یکی است؟

نه، دو خطای متفاوت هستند. Maximum execution time خطای PHP است که وقتی محدودیت خود PHP سر می‌رسد رخ می‌دهد. Gateway Timeout خطای وب‌سرور یا پروکسی است که وقتی پاسخی از سرور در زمان مشخصی دریافت نمی‌کند برمی‌گردد. ممکن است اولی منجر به دومی شود، ولی ریشه و راه‌حل متفاوت است.

آیا این خطا روی SEO اثر دارد؟

بله، غیرمستقیم. اگر خزنده‌ی گوگل هنگام بازدید از صفحه، خطای timeout دریافت کند، آن صفحه را در رتبه‌بندی پایین‌تر می‌آورد. اگر نرخ timeout در سایت بالا باشد، بودجه‌ی خزش مصرف می‌شود و صفحات کمتری از سایت ایندکس می‌شوند. تجربه‌ی کلی کاربر هم تحت تأثیر قرار می‌گیرد که بر سئو اثر می‌گذارد.

آنچه از سال‌ها کار با خطای Timeout در PHP یاد گرفتم

اگر بخواهم چکیده‌ی سال‌ها کار با این خطا را در چند جمله بگویم، سه اصل عملی است که در تمام پروژه‌هایم رعایت می‌کنم:

یک: تشخیص پیش از راه‌حل. خطای timeout همیشه یک علامت است، نه یک بیماری. قبل از هر اقدامی، باید ریشه را پیدا کنید. در تجربه‌ی من، هفتاد درصد موارد در سه گام اول تشخیص حل می‌شوند. بیست و پنج درصد بعدی نیاز به بازنویسی دارند. پنج درصد باقی‌مانده، نیاز به تغییرات معماری دارند.

دو: هرگز timeout را به‌عنوان راه‌حل نپذیرید. چه با افزایش مقدار، چه با حذف کامل. timeout، حصار امنیتی سرور است. این حصار را می‌توان با نرخ مناسب تنظیم کرد، ولی نباید حذف کرد. حجم ترافیک سایت شما، به‌سرعت با حذف این حصار به کابوس تبدیل می‌شود.

سه: برای کارهای سنگین، معماری درست بچینید. Queue، Batch، و Cache سه ابزار اصلی در این حوزه هستند. اگر این سه ابزار در پروژه‌ی شما وجود داشته باشد، خطای timeout تقریباً از بین می‌رود. اگر ندارند، اولین تسک سنگین، اولین خطای timeout را تولید می‌کند.

خطای Maximum execution time در PHP، در نگاه اول ساده به‌نظر می‌رسد. ولی وقتی آن را در چارچوب کلی معماری می‌بینیم، می‌فهمیم که یک سیگنال از طرف زبان است. این سیگنال می‌گوید اسکریپت شما در محیطی که برای آن طراحی شده، کار درستی انجام نمی‌دهد. اگر این سیگنال را جدی بگیرید و به‌جای پنهان کردنش، معماری را اصلاح کنید، پروژه‌ی شما به سطحی از پایداری می‌رسد که هیچ مقدار افزایش timeout نمی‌تواند ایجاد کند.

هدف این مقاله، تمام‌کردن همه‌ی سناریوهای ممکن نبود. هدف، دادن یک چارچوب ذهنی برای تشخیص، بازنویسی و پایش این خطا بود. وقتی این چارچوب را درونی کنید، برخورد با این خطا از یک واکنش اضطراری به یک فرآیند منظم تبدیل می‌شود. و این، همان تفاوتی است که بین تیم‌های معمولی و تیم‌های حرفه‌ای در بلندمدت وجود دارد.

اگر خطای Maximum execution time در پروژه‌ی شما به شکلی ظاهر شده که با الگوهای این مقاله حل نشده، برای من جالب است بدانم کدام سناریو بود. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حلی پیدا کرده‌اید که هنوز در این مقاله نیست. ⏱️